AI Research Library

Tool Use as Learned API Interaction

Rajesh Ranjan Mahapatra

An agent has not learned a tool when it can emit valid JSON; it has learned the tool only when it can choose an applicable call, anticipate its state change, interpret typed failure and recover without duplicating effects.

© 2026 Rajesh Ranjan Mahapatra. 31 min read

Tool Use as Learned API Interaction

An agent has not learned a tool when it can emit valid JSON; it has learned the tool only when it can choose an applicable call, anticipate its state change, interpret typed failure and recover without duplicating effects.

TLDR

  1. Tool competence is a policy over a stateful contract, not a score for matching function names and arguments.
  2. The contract joins schema, preconditions, effects and error semantics, so the same valid call can lead to execute, repair, refresh, read back, defer or stop.
  3. Expose semantic choices to the model, but enforce identity, permission, freshness, idempotency and effect limits in a deterministic execution boundary.
  4. Complete contracts still fail when world state is hidden, services violate their documentation or effects cannot be independently observed.
  5. Admit a tool only after stateful recovery tests show verified outcomes under stale state, typed errors, timeouts and duplicate attempts.

The refund that succeeded twice

A customer asks a retail assistant to credit £25 against order O-417. The model chooses the correct function and produces a syntactically perfect request. The order exists, its status permits a credit, the amount is within the refundable balance and the request reaches the service. The service commits the credit. Before the response arrives, the connection closes.

The assistant sees a timeout. It reasons that the operation probably failed, creates a fresh request identifier and calls the same function again. The second call also passes the schema. The remaining balance still permits £25, so the service commits a second credit. The customer receives £50. Every emitted object was well formed. Every function name was correct. The failure appeared only when two valid calls were interpreted as one intended action.

A well-typed call is not yet a correct action. Shape validation can prove that required fields exist and values have allowed types. It cannot prove that the call applies to the current world, that the caller may perform it, that the server produced the intended effect or that retrying after an ambiguous response is safe. Those are different claims, supported by different evidence.

The central answer follows. Tool use is learned API interaction when the model learns a policy over one contract containing schema, preconditions, effects and errors. Given an intent, an observed world state, a tool description and an interaction history, it must choose among more than API names. It must decide whether to call, ask, refresh, repair, verify, retry, abandon or escalate. A deterministic gateway then checks what language-model judgement must never be allowed to grant: identity, authority, current entitlement and hard effect limits.

This question is narrower than trajectory assurance across an entire agent decision. Trajectory assurance asks how to evidence the whole path. Here the prior question is whether each tool exposes enough semantics for a path to be meaningfully evaluated at all.

Part oneThe call is a state transition

A function signature invites a familiar picture: choose a name, fill its parameters, receive a value. That picture is adequate for a pure calculator. It becomes misleading as soon as the operation reads changing state, writes durable state, depends on policy or can finish without returning a response.

Represent a tool contract as C = (Σ, P, Δ, E). The schema Σ defines the language of structurally valid actions. The precondition predicate P(s, a) says whether action a applies in world state s. The transition relation Δ(s, a) describes possible new states and observations. The error semantics E(o) maps an observation to what it means for the next decision. The user intent supplies a postcondition Q, such as “one £25 credit exists for this request”.

a ∈ L(Σ) ∧ P(s, a) ∧ s′ ∈ Δ(s, a) ∧ Q(s, s′, intent)

Here L(Σ) is the set of requests accepted by the schema; s′ is the resulting world state. Each term removes a different failure. Tightening Σ reduces malformed requests. Improving P reduces calls made in the wrong situation. Modelling Δ reveals side effects and uncertain outcomes. Improving E changes recovery after a conflict, timeout or refusal. No single term substitutes for the others.

The interface is a four-layer contract

An API call surrounded by schema, precondition, effect and error layers A central refund call is enclosed by four irregular contours. The contours represent request shape, applicability, state transition and recovery semantics. issue_credit model proposal Σ schema P preconditions Δ effects and observations E errors and recovery A call crosses every layer, even when only the inner schema is machine-readable.
Figure 1. The mandatory annotated tool interface. Practitioner synthesis. The four contours are conceptual, not measured. Their uneven shape is deliberate: real contracts often specify the inner request precisely while leaving outer operational meaning implicit.

Thought experiment one: hold the JSON fixed

Place the same valid credit request in two worlds. In World A, the order is delivered, has £50 remaining and has no prior credit under the request key. In World B, the order is still pending. Change nothing else. The tool name and arguments remain identical. A schema-only evaluator awards the same score to both. A stateful contract executes A and rejects B.

Applicability is a relation between an action and a world, not a property of the action string. Training examples that pair a user utterance directly with one gold function call conceal this causal variable. The model can learn textual association while never learning which observation makes the action legal or useful.

One valid call, two different worlds

Figure 2. Synthetic intervention. Only order status changes. The schema result stays constant while the correct action changes, isolating precondition understanding from argument formation.

Thought experiment two: hold the timeout fixed

Now keep the observation fixed: DEADLINE_EXCEEDED. Change only the operation. The first operation reads a weather forecast. Repeating it may spend time, but it does not intentionally change the forecast. The second issues credit. The first request may have committed before the response disappeared. An automatic retry could duplicate value.

The error token alone is therefore incomplete. Correct recovery depends on the operation's intended effect, idempotency mechanism and available readback. RFC 9110 defines an idempotent method by whether repeated identical requests have the same intended server effect, and cautions against automatically retrying non-idempotent requests without additional knowledge.[12] A stable request identifier can supply that knowledge by letting the service return the prior result for a duplicate request.[15]

A timeout after a state-changing call is an unknown outcome, not evidence of failure. The next action should often be readback, not repetition. That decision cannot be learned from request syntax alone.

The same timeout demands different recovery

A timeout observation branches into retry or readback depending on the operation A central timeout node feeds an upper path for a pure read that can be retried and a lower path for a state-changing credit that requires readback and stable idempotency. TIMEOUT observation fixed Pure read retry Credit read back Same transport signal Different effect semantics No intended write Commit may already exist
Figure 3. Synthetic thought experiment. Only the operation's effect changes. The timeout observation is held constant, showing why error handling belongs to the same contract as effects.

Negative control: the pure converter

Consider celsius_to_fahrenheit(value). It has no external state, durable effect, permission decision or ambiguous commit. Given a numeric input, the output follows deterministically. Here a schema, a short description and a few boundary tests can be enough. A heavy workflow contract adds ceremony without changing the answer.

This control matters because it blocks an inflated conclusion. The claim is not that every function requires orchestration. It is that contract depth should track state dependence and consequence. Generate-and-validate is a credible baseline for pure, reversible and independently checkable tools. The full mechanism earns its cost when hidden state, policy, side effects or uncertain outcomes can change the next action.

Minimal worked example: when the contract collapses cleanly

Worked example

Give a model one tool: convert_temperature(value, from_unit, to_unit). Its schema permits finite numbers and the symbols C, F and K. Its result is a number plus the target unit. The operation reads no external state and requests no durable change. For a fixed implementation, the same valid arguments produce the same intended result regardless of customer history, time or concurrent activity.

The evaluation can therefore be compact. A positive case checks that 0°C becomes 32°F. A boundary case checks that −40 has the same numeric value in Celsius and Fahrenheit. A negative case supplies K below absolute zero and expects a typed invalid-argument response. A metamorphic property converts C to F and back, allowing a stated floating-point tolerance. These tests exercise meaning beyond JSON shape, yet none requires an order version, effect receipt or recovery workflow.

Now change one feature. Suppose the “converter” also stores a billable laboratory calibration record. The numeric mapping has not changed, but the operation is no longer pure. The contract must state who may create the record, whether repetition creates a second charge, how a timeout is resolved and how the new record is read back. The extra machinery follows the added effect, not the presence of an LLM. Contract depth should grow when causal exposure grows.

The argument in one passChart · flow
flowchart LR
  A["The refund that succeeded twice"] --> B["Part one The call is a state<br/>transition"]
  B --> C["Part two When state entered<br/>the benchmark"]
  C --> D["Part three Compile a contract<br/>the model and executor…"]
  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 structural route through Tool Use as Learned API Interaction, from The refund that succeeded twice to Sources. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Part twoWhen state entered the benchmark

Published evidence

Tool-use research did not begin with a single agreed construct. Toolformer trained a model to decide which APIs to call, when to call them, which arguments to supply and how to incorporate returned results.[1] API-Bank supplied 73 runnable tools and evaluated planning, retrieval and calls across 314 annotated dialogues containing 753 calls.[2] Gorilla focused on writing calls against large, changing API collections and used retrieval-aware training to adapt to documentation changes.[3] ToolLLM expanded coverage to 16,464 collected REST APIs and generated multi-tool solution paths.[4]

These projects established that tool choice and argument formation are learnable behaviours. They also made documentation a training object rather than an implementation afterthought. One study across six tasks reported that documentation-only prompts could match few-shot tool demonstrations in its evaluated settings.[5] That result supports exposing precise descriptions. It does not show that prose documentation alone supplies current world state, authority or effect verification.

The evaluation boundary then widened. BFCL covers serial and parallel calls, abstention and stateful multi-step settings; its 2025 paper reports that single-turn calls are much stronger than memory, dynamic decisions and long-horizon behaviour.[6] ToolSandbox adds stateful execution, implicit dependencies, on-policy conversation and milestone checks.[8] τ-bench evaluates the final database state after conversations governed by domain policies and introduces pass^k to ask whether repeated runs all succeed, rather than whether one lucky sample succeeds.[7]

The construct changed from “can the model write a call?” to “can the agent complete a stateful interaction consistently?” That is more than benchmark inflation. It reveals the mechanism that single-turn scoring omits. Once actions modify a shared world, later observations depend on earlier actions, and a locally plausible call can destroy the only successful trajectory.

Tool evaluation accumulated missing causal variables

Figure 4. Sourced conceptual timeline, not a performance ranking. Each milestone is supported by references 1 to 8. Later benchmarks do not make earlier ones useless; they test additional variables that become causal in long-running interaction.

The strongest objection: let the executor handle everything

A serious competing design says the model should only produce candidate arguments. A deterministic client library can validate them, perform retries, manage versions and return a clean success or failure. This is the right baseline. Transport retries, authentication refresh, request de-duplication and schema enforcement should be removed from language-model discretion whenever their rules are mechanical.

The baseline stops short when recovery changes the meaning of the task. A version conflict may reveal that a customer already cancelled the order. A failed precondition may require a new user decision. A partial result may satisfy one branch of a compound request while invalidating another. A tool may return two alternatives whose choice depends on preference stated earlier in the conversation. A wrapper can make these facts typed and safe; it cannot choose their semantic consequence without an intent-sensitive policy.

Use the narrowest intelligent boundary. Let deterministic code perform operations whose correct next step is fixed by protocol. Let the model choose only where the next step depends on natural-language intent, ambiguous evidence or trade-offs that must be explained.

Part threeCompile a contract the model and executor can share

OpenAPI defines a language-agnostic description of HTTP interfaces and gives operations identifiers, parameters, request bodies and possible responses.[9] JSON Schema can assert what a valid JSON document must look like.[11] Arazzo adds multi-step workflow semantics including success criteria, failure actions, outputs and timeouts.[10] These standards provide strong raw material. A model-facing contract still needs a compiled view that connects them to the decision the model must make.

A useful compiler emits two related artefacts. The model contract contains concise purpose, applicability, effect and recovery language with examples and counterexamples. The execution contract contains enforceable schemas, identity and permission requirements, version checks, idempotency rules, timeouts, observability fields and postcondition queries. Both derive from one governed source, but they serve different consumers.

Do not ask prose to enforce what code can prove, and do not ask code to infer what the user meant. The model should see that a timeout may mean “effect unknown” and that a version conflict requires fresh state. The gateway should ensure the same request key is reused, block calls without authority and refuse an obsolete version even if the model insists.

Describe effects as transitions, not marketing verbs

Names such as update, submit and process are weak descriptions because they hide what persists. An effect declaration should identify the resource changed, the fields or invariant affected, the version rule, any secondary effects and the observation that proves completion. “Issues a credit” is weaker than “creates one credit receipt keyed by request_id, increments the order's credited total and resource version, and never changes the refundable total”.

Preconditions need the same treatment. Write them as predicates whose evidence can be obtained: order.status == DELIVERED, amount <= refundable_remaining and expected_version == current_version. Record the source and freshness of each value. A condition inferred from an old conversation is not equivalent to a condition read from the order service. For HTTP resources, validators such as entity tags and conditional requests can prevent a stale write from silently overwriting a newer representation.[12]

The compiled contract should also name uncertainty. A response can establish accepted, rejected, committed, not committed or unknown. These states should not be compressed into a Boolean success flag. A 202 response may mean work has merely started. A dropped connection may hide a completed write. A business rejection can arrive in a technically successful transport response. The recovery policy needs the semantic state, while the execution boundary needs the raw evidence that justifies it.

Finally, version the contract independently from the model. An operation can retain its name while changing required fields, side effects or retry guarantees. Store the contract version with each proposal and execution receipt. Retrieval-aware systems such as Gorilla show why documentation change matters for call formation.[3] The same principle is more important for effects: a model that learned yesterday's retry rule can be structurally correct and operationally dangerous today.

The contract compiler is a release control

In practice, the source material is scattered. Request and response shapes live in OpenAPI or protobuf. Preconditions sit in service code, product rules or runbooks. Retry behaviour may exist only in a client library. Permissions belong to an identity and policy system. Outcome evidence may require a separate query. A contract compiler does not invent these facts. It joins them into a versioned intermediate representation and fails the build when a consequential gap remains.

For each operation, compilation should answer five tests. Can every precondition be resolved from an authoritative source? Does every write name its intended and possible secondary effects? Does every typed error map to a bounded next-action class? Can an ambiguous commit be resolved by request key or readback? Can the executor check authority without trusting text produced by the model? A missing answer is not a documentation polish item. It is an unmodelled branch in the action space.

The compiler can then generate different views. The model receives a compact description, relevant examples, counterexamples and typed observations. The gateway receives validators, policy hooks, concurrency tokens, retry rules and effect limits. The evaluator receives initial-state builders, fault injectors and postcondition queries. Documentation receives human-readable purpose and failure notes. Because the views share one operation identifier and contract version, a changed precondition can trigger prompt tests, gateway tests and benchmark regeneration together.

This is a practitioner synthesis, not a claim that current interface standards provide such a compiler end to end. OpenAPI and Arazzo supply important syntax; service owners must still expose business state, authority and outcome observers. The release rule is simple: do not publish a consequential tool to an agent catalogue when its interaction contract cannot be compiled without unresolved effect or recovery semantics.

The learned policy sits inside a deterministic control loop

A circular tool-use loop with separate model proposal and deterministic execution Intent and context feed a model proposal. A deterministic gateway checks schema, state, authority and idempotency, executes against the world, obtains an observation and readback, and returns typed evidence for the next model decision. Intent + context observed state, contract Model proposal call, ask, stop, verify Gateway shape, state, authority idempotency, limits World transition effect may be partial Observation typed error + readback Learn the next decision
  1. Intent and contextObserved state and the versioned contract
  2. Model proposalCall, ask, stop or verify
  3. Deterministic gatewayShape, state, authority, idempotency and limits
  4. World transitionThe effect may be complete, partial or absent
  5. Typed observation and readbackEvidence for the next decision
Figure 5. Practitioner architecture. The topology is the argument: model proposal and deterministic authority are separate nodes in a closed evidence loop. No performance numbers are implied.

Errors are observations with action semantics

Free-text exceptions force the model to guess. RFC 9457 standardises machine-readable problem details for HTTP APIs.[13] Google's API guidance similarly recommends canonical codes and actionable details, then distinguishes errors that may be retried automatically from those where repetition would cause unintended state changes.[14][16] The precise taxonomy will differ by service, but the next-action meaning should not.

Typed observationWhat it saysDefault next decisionEvidence needed
INVALID_ARGUMENTThe proposed object is wrong for the declared interface.Repair arguments or ask for missing information.Validation path and offending value.
FAILED_PRECONDITIONThe call is shaped correctly but does not apply now.Stop, choose another path or explain the blocked condition.Current state and violated predicate.
VERSION_CONFLICTThe decision used stale world state.Refresh, reconsider intent and replan.New version and changed fields.
UNAVAILABLEThe service could not begin or complete a known-safe operation.Retry only under a documented policy.Attempt identifier and retry class.
UNKNOWN_OUTCOMEThe effect may exist although no success response arrived.Read back by stable request key before any retry.Effect receipt or authoritative state.
PERMISSION_DENIEDThe caller lacks authority for the effect.Do not route around the control. Escalate through an approved path.Policy decision and principal binding.

The last row marks the system boundary. A model may understand what permission means, but a model proposal never becomes authority by describing itself confidently. Identity, actor, subject, purpose and permission must remain mechanically bound at execution time.

Worked scenario: a refund as a governed transition

Worked scenario

Use a synthetic retail platform with four tools: get_order, quote_credit, issue_credit and get_credit_receipt. The customer asks for £25. The order record is current world state. The tool documentation is context. The model's temporary deliberation is reasoning state. The sequence of calls and responses is an evidence ledger. Calling all four “memory” would erase the distinctions needed for recovery.

The interaction starts by binding intent to order O-417. A read returns status DELIVERED, refundable balance £50 and version 7. The quote tool calculates the permissible amount without changing state. After any required confirmation, the model proposes issue_credit with amount £25, expected version 7 and a stable idempotency key derived from the approved intent.

The gateway checks schema, principal, purpose, amount limit, current version and request-key format. The service executes. Success is not the HTTP status alone. The system reads the receipt by idempotency key and verifies that exactly one £25 effect exists. It records both the accepted command and the observed state. Only then does it tell the user that the credit was issued.

A production-shaped transition carries evidence forward

Figure 6. Synthetic production-shaped sequence. The money values and order are invented. The sequence separates proposal, permission, accepted command, effect evidence and user-facing outcome.

Place the contract inside the full control boundary

The tool contract touches ten unlike objects. Intent states the desired change. Identity binds the requester and represented subject. World state describes current reality. Context supplies relevant documentation and policy. Reasoning proposes the next move. Authority decides whether that move may occur. Action is the typed command. Evidence records what was accepted and observed. Outcome is the verified business state. Release and recovery determine what can continue after failure.

Collapsing these objects creates predictable errors. An API connection is not authority. A retrieved policy paragraph is not current world state. A model explanation is not an effect receipt. A successful transport response is not a verified outcome. The interface becomes governable only when each claim points to the object that can support it.

Part fourTrain the recovery policy

If the desired capability is interaction, the training and evaluation unit must be an episode. Each episode needs an initial world state, user intent, contract version, permitted observations, fault schedule and target postcondition. A gold call is useful inside the episode, but it cannot stand in for the episode.

A balanced suite varies one causal feature at a time. Hold the user request fixed and change order status. Hold state fixed and remove one required argument. Hold both fixed and inject a version conflict. Commit the effect and drop the response. Return a transient error before commit. Change the documentation version. Withhold a required fact so the correct action is to ask. Include irrelevant but similarly named tools. Revoke confirmation after the quote. Each case tests a distinct part of the policy.

The scorecard should preserve those distinctions. Report structural validity, tool-selection precision, precondition violations, clarification quality, state-transition success, duplicate-effect rate, recovery correctness, abstention, effect verification and repeated-run consistency. A single end score can still be useful for release, but it should be decomposable into failure mechanisms.

L = λΣLΣ + λselectLselect + λpreLpre + λeffectLeffect + λrecoverLrecover + λstopLstop

Each L measures one error family; each weight λ reflects consequence. Raising λeffect penalises wrong or duplicated state changes more heavily. Raising λstop rewards abstention when information is missing. The equation is a design template, not a published universal objective. Its value is that it prevents high schema accuracy from numerically hiding effect failure.

Format accuracy can stay high while outcome success falls

Illustrative plot of schema accuracy and verified outcome success as state dependence increases A nearly flat indigo line represents call format accuracy. A falling coral line represents verified outcome success as tool interactions become more state dependent. Illustrative success index State dependence and consequence pure stateful Schema-valid calls Verified outcomes hidden gap
Illustrative mobile plot of format accuracy and verified outcome success Schema-valid calls remain almost flat while verified outcomes fall as state dependence rises. Illustrative success State dependence pure stateful Schema-valid calls Verified outcomes hidden gap
Figure 7. Explicitly illustrative relationship. The curves are not fitted to a benchmark and have no numeric denominator. They show the hypothesis the recovery suite is designed to test: structural accuracy can remain stable as unmodelled state dependence increases outcome failure.

What the model should actually learn

The model does not need to memorise TCP behaviour or reproduce a client library. It needs decision-sufficient abstractions. Examples include: “this operation changes durable state”; “this precondition must be observed, not assumed”; “this conflict invalidates the old plan”; “this timeout leaves the outcome unknown”; “this readback can prove the effect”; and “this refusal cannot be repaired by choosing a different tool”.

Training traces should pair those abstractions with action choices, not reward verbose private narration. A concise typed state can carry effect_class=WRITE, retry_class=READBACK_FIRST, version=7 and confirmation=BOUND. The model's job is to map the situation to the next semantic move. The gateway's job is to enforce the move and return a typed observation.

Error recovery is part of tool competence because errors change the action distribution. Removing recovery examples trains a policy for a world in which services never conflict, users never omit information and networks never fail after commit. That is not a simplified production environment. It is a different causal environment.

A training episode is a small causal experiment

An episode manifest should name the intent, initial public state, hidden evaluator state, contract version, available tools, observation budget, injected fault and target postcondition. It should also state which actions are forbidden even if they accidentally reach the target state. That last field prevents an evaluator from rewarding a credit issued without confirmation merely because the final database matches.

Construct matched pairs. In one pair, the only difference is status=DELIVERED versus status=PENDING. In another, the service fails before commit versus after commit. In a third, a required fact is present versus absent. Randomising names, amounts and irrelevant descriptions reduces memorisation, while preserving the causal relation being tested. Hold everything else fixed so a score difference can be attributed to the changed feature rather than to prompt difficulty.

The evaluator should inspect observable decisions, not demand a private chain of thought. It can record selected tool, arguments, state reads, clarification questions, gateway decisions, typed errors, retries, readbacks and final state. Milestone checks catch a trajectory that reaches the right endpoint through a prohibited intermediate action. Final-state checks catch a fluent trajectory whose effect never occurred. Repeated trials estimate consistency; the pass^k framing from τ-bench is useful because consequential systems need all repeated runs to be correct, not one successful sample among many.[7]

Split evaluation by novelty. One partition varies values inside familiar schemas. Another introduces unseen but well-documented tools. A third changes a familiar tool's contract version. A fourth composes tools whose individual calls were seen but whose state dependency was not. Improvement limited to the first partition suggests memorised call patterns. Transfer to the others is stronger evidence that the model learned contract structure.

Executable recovery benchmark

The included Python 3.10+ artefact creates a synthetic credit API with deterministic fault injection. It tests a governed recovery policy against a happy path, a concurrent version change, unavailability before commit, timeout after commit, two failed preconditions and a blind-retry negative control. No model vendor, network or private data is required.

Its critical property is matched intent. In the timeout case, the governed client reads back the stable idempotency key and records one effect. The naïve client treats the same timeout as failure, changes the key and creates a second £25 credit. The negative case therefore reveals logic that a prose example could conceal.

The recovery harness distinguishes retry from duplicate effect

ScenarioExpectedObservedEffectsCalls
Happy pathVerifiedVerified11
Version conflictVerifiedVerified12
Transient before commitVerifiedVerified12
Timeout after commitVerifiedVerified by readback11
Failed business preconditionBlockedBlocked01
Insufficient balanceBlockedBlocked00
Blind retry with new keyDetectedDuplicated22
Figure 8. Measured by the included synthetic script in one deterministic run. “Effects” counts created credit receipts, not money movement in a real system. Assertions fail if any governed case changes status or effect count.
Run the complete Python artefact

Save as tool_contract_recovery_benchmark.py and run python tool_contract_recovery_benchmark.py. Expected output is the table above followed by the two-step negative-control trace. Assumptions: a single synthetic order, integer currency units, deterministic faults, server-side request-key de-duplication and authoritative receipt readback.

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Iterable


class Code(str, Enum):
    OK = "OK"
    INVALID_ARGUMENT = "INVALID_ARGUMENT"
    FAILED_PRECONDITION = "FAILED_PRECONDITION"
    VERSION_CONFLICT = "VERSION_CONFLICT"
    UNAVAILABLE = "UNAVAILABLE"
    UNKNOWN_OUTCOME = "UNKNOWN_OUTCOME"


@dataclass
class Order:
    order_id: str
    status: str
    version: int
    refundable_total: int
    credited_total: int = 0

    @property
    def refundable_remaining(self) -> int:
        return self.refundable_total - self.credited_total


@dataclass(frozen=True)
class Receipt:
    receipt_id: str
    order_id: str
    amount: int
    idempotency_key: str


@dataclass(frozen=True)
class ToolResult:
    code: Code
    message: str
    receipt: Receipt | None = None


@dataclass
class Trace:
    steps: list[str] = field(default_factory=list)

    def add(self, text: str) -> None:
        self.steps.append(text)


class SyntheticCreditAPI:
    """A stateful synthetic API with deterministic fault injection."""

    def __init__(self, order: Order, faults: Iterable[str] = ()) -> None:
        self.order = order
        self.faults = list(faults)
        self.receipts_by_key: dict[str, Receipt] = {}
        self.receipts: list[Receipt] = []
        self.calls = 0

    def get_order(self, order_id: str) -> Order:
        if order_id != self.order.order_id:
            raise KeyError(order_id)
        # Return a snapshot, not a reference to mutable server state.
        return Order(**vars(self.order))

    def get_credit_receipt(self, idempotency_key: str) -> Receipt | None:
        return self.receipts_by_key.get(idempotency_key)

    def issue_credit(
        self,
        *,
        order_id: str,
        amount: int,
        expected_version: int,
        idempotency_key: str,
    ) -> ToolResult:
        self.calls += 1

        # A stable request identifier turns an identical retry into a readback.
        prior = self.receipts_by_key.get(idempotency_key)
        if prior is not None:
            return ToolResult(Code.OK, "duplicate request returned prior result", prior)

        if order_id != self.order.order_id or amount <= 0:
            return ToolResult(Code.INVALID_ARGUMENT, "unknown order or non-positive amount")
        if self.order.status != "DELIVERED":
            return ToolResult(Code.FAILED_PRECONDITION, "order is not delivered")
        if amount > self.order.refundable_remaining:
            return ToolResult(Code.FAILED_PRECONDITION, "amount exceeds refundable balance")
        if expected_version != self.order.version:
            return ToolResult(Code.VERSION_CONFLICT, "order changed; refresh before retry")

        fault = self.faults.pop(0) if self.faults else "none"
        if fault == "version_conflict_once":
            self.order.version += 1  # A concurrent, non-crediting update.
            return ToolResult(Code.VERSION_CONFLICT, "concurrent update changed version")
        if fault == "unavailable_before_commit":
            return ToolResult(Code.UNAVAILABLE, "service unavailable before commit")

        receipt = Receipt(
            receipt_id=f"rcpt-{len(self.receipts) + 1}",
            order_id=order_id,
            amount=amount,
            idempotency_key=idempotency_key,
        )
        self.order.credited_total += amount
        self.order.version += 1
        self.receipts.append(receipt)
        self.receipts_by_key[idempotency_key] = receipt

        if fault == "timeout_after_commit":
            # The client did not receive a success response, but the effect exists.
            return ToolResult(Code.UNKNOWN_OUTCOME, "connection lost after possible commit")
        return ToolResult(Code.OK, "credit committed", receipt)


def verify_exactly_once(
    api: SyntheticCreditAPI,
    *,
    key: str,
    amount: int,
    before_total: int,
    before_receipts: int,
) -> bool:
    receipt = api.get_credit_receipt(key)
    return bool(
        receipt
        and receipt.amount == amount
        and api.order.credited_total == before_total + amount
        and len(api.receipts) == before_receipts + 1
    )


def governed_issue_credit(
    api: SyntheticCreditAPI,
    *,
    order_id: str,
    amount: int,
    idempotency_key: str,
    max_attempts: int = 3,
) -> tuple[str, Trace]:
    """A deterministic recovery policy around a stochastic model proposal."""

    trace = Trace()
    before_total = api.order.credited_total
    before_receipts = len(api.receipts)
    snapshot = api.get_order(order_id)
    trace.add(f"read state: version={snapshot.version}, remaining={snapshot.refundable_remaining}")

    if amount <= 0 or amount > snapshot.refundable_remaining:
        trace.add("stop: local precondition failed")
        return "BLOCKED", trace

    for attempt in range(1, max_attempts + 1):
        result = api.issue_credit(
            order_id=order_id,
            amount=amount,
            expected_version=snapshot.version,
            idempotency_key=idempotency_key,
        )
        trace.add(f"attempt {attempt}: {result.code.value}")

        if result.code is Code.OK:
            if verify_exactly_once(
                api,
                key=idempotency_key,
                amount=amount,
                before_total=before_total,
                before_receipts=before_receipts,
            ):
                trace.add("readback: intended effect exists exactly once")
                return "VERIFIED", trace
            trace.add("escalate: response and postcondition disagree")
            return "ESCALATE", trace

        if result.code in {Code.INVALID_ARGUMENT, Code.FAILED_PRECONDITION}:
            trace.add("stop: repair needs new intent or user information")
            return "BLOCKED", trace

        if result.code is Code.VERSION_CONFLICT:
            snapshot = api.get_order(order_id)
            trace.add(f"refresh: version={snapshot.version}, remaining={snapshot.refundable_remaining}")
            if amount > snapshot.refundable_remaining:
                trace.add("stop: refreshed state invalidates proposal")
                return "BLOCKED", trace
            continue

        if result.code is Code.UNAVAILABLE:
            trace.add("retry: same arguments and same idempotency key")
            continue

        if result.code is Code.UNKNOWN_OUTCOME:
            receipt = api.get_credit_receipt(idempotency_key)
            if receipt is not None:
                trace.add("readback after timeout: committed effect found")
                if verify_exactly_once(
                    api,
                    key=idempotency_key,
                    amount=amount,
                    before_total=before_total,
                    before_receipts=before_receipts,
                ):
                    return "VERIFIED", trace
                return "ESCALATE", trace
            trace.add("no receipt found: retry with the same idempotency key")
            continue

    trace.add("escalate: recovery budget exhausted")
    return "ESCALATE", trace


def naive_issue_credit(api: SyntheticCreditAPI, *, order_id: str, amount: int) -> tuple[str, Trace]:
    """Negative case: retry a timeout as a new request without readback."""

    trace = Trace()
    snapshot = api.get_order(order_id)
    first = api.issue_credit(
        order_id=order_id,
        amount=amount,
        expected_version=snapshot.version,
        idempotency_key="naive-attempt-1",
    )
    trace.add(f"attempt 1: {first.code.value}")
    if first.code is Code.UNKNOWN_OUTCOME:
        refreshed = api.get_order(order_id)
        second = api.issue_credit(
            order_id=order_id,
            amount=amount,
            expected_version=refreshed.version,
            idempotency_key="naive-attempt-2",  # A new key defeats de-duplication.
        )
        trace.add(f"blind retry with new key: {second.code.value}")
    return "DUPLICATED" if len(api.receipts) == 2 else "NOT_DUPLICATED", trace


def run_benchmark() -> None:
    cases = [
        ("happy path", (), Order("O-417", "DELIVERED", 7, 50), "VERIFIED"),
        ("version conflict", ("version_conflict_once",), Order("O-417", "DELIVERED", 7, 50), "VERIFIED"),
        ("transient before commit", ("unavailable_before_commit",), Order("O-417", "DELIVERED", 7, 50), "VERIFIED"),
        ("timeout after commit", ("timeout_after_commit",), Order("O-417", "DELIVERED", 7, 50), "VERIFIED"),
        ("failed business precondition", (), Order("O-417", "PENDING", 7, 50), "BLOCKED"),
        ("insufficient refundable balance", (), Order("O-417", "DELIVERED", 7, 20), "BLOCKED"),
    ]

    print(f"{'scenario':34} {'expected':10} {'actual':10} {'effects':7} {'calls':5}")
    print("-" * 72)
    for name, faults, order, expected in cases:
        api = SyntheticCreditAPI(order, faults)
        actual, trace = governed_issue_credit(
            api,
            order_id="O-417",
            amount=25,
            idempotency_key=f"credit:{name}:O-417:25",
        )
        assert actual == expected, (name, actual, trace.steps)
        expected_effects = 1 if expected == "VERIFIED" else 0
        assert len(api.receipts) == expected_effects, (name, api.receipts)
        print(f"{name:34} {expected:10} {actual:10} {len(api.receipts):7} {api.calls:5}")

    naive_api = SyntheticCreditAPI(
        Order("O-417", "DELIVERED", 7, 50),
        faults=("timeout_after_commit",),
    )
    actual, trace = naive_issue_credit(naive_api, order_id="O-417", amount=25)
    assert actual == "DUPLICATED"
    assert naive_api.order.credited_total == 50
    print(f"{'negative control: blind retry':34} {'DETECTED':10} {actual:10} {len(naive_api.receipts):7} {naive_api.calls:5}")
    print("\nNegative-control trace:")
    for step in trace.steps:
        print(f"  - {step}")


if __name__ == "__main__":
    run_benchmark()

Decision instrument: admit the interaction, not the endpoint

A tool registry should reject operations whose contract cannot support the decisions their consequences require. The instrument below is intentionally stricter for writes than reads. It is a starting gate for design and evaluation, not evidence that an admitted tool is universally safe.

Tool admission card

1. PurposeOne sentence describing when the tool advances a user intent, plus a counterexample.
2. Structural schemaClosed, versioned input and output shapes with units, bounds and nullable fields.
3. PreconditionsObservable predicates, their source of truth and the freshness required before execution.
4. Effect classPure read, reversible write, compensable write or irreversible effect.
5. PostconditionAn independent query or receipt that can establish the intended state change.
6. Error vocabularyTyped failures with action semantics: repair, refresh, retry, read back, stop or escalate.
7. Duplicate controlStable request key, honouring period and behaviour when a prior result is found.
8. Authority boundaryActor, subject, purpose, permission and confirmation checked outside the model.
9. Recovery testsPositive, negative, stale-state, pre-commit failure and post-commit timeout cases.
Reject whenA consequential effect lacks current precondition evidence, typed failure semantics or independent readback.
Figure 9. Practitioner decision instrument. Fields derive from the paper's mechanism and official interface guidance; thresholds must be set by the use case's consequence and recovery cost.

Keep operating evidence without copying the whole conversation

A recoverable interaction needs a compact trace. Record the contract version, proposal digest, bound actor and purpose, state snapshot identifiers, gateway decision, request key, attempt number, typed observation, readback query and effect receipt. These fields support replay and incident analysis without requiring every private prompt or retrieved document to be duplicated into logs.

Separate a decision receipt from an effect receipt. The first says why a command was admitted: which intent, evidence, policy and version supported it. The second says what the executing system observed after the command. Their identifiers should join across traces, while sensitive payloads remain in their governed stores. OpenTelemetry's stable HTTP span conventions provide a common transport vocabulary, including retry and error attributes, but application fields are still required to express business preconditions and verified outcomes.[17]

Evidence retention also needs limits. High-cardinality customer data should not become metric labels. Error messages may contain private values and should be redacted or referenced. A trace that exposes everything can make recovery easier while creating a new privacy failure. Operating evidence should preserve the causal joins needed for verification, not become an unbounded copy of context.

Where the mechanism stops helping

Explicit failure boundary

A complete declared contract cannot repair a false world model. If the authoritative state is inaccessible, stale or internally inconsistent, the precondition check can certify the wrong world. If the service commits effects it does not document, returns misleading errors, ignores request identifiers or offers no independent readback, the recovery policy lacks the observations needed to distinguish success, failure and partial effect.

The mechanism also does not solve authority by itself. An impeccably trained model can still propose a prohibited action. Nor does it guarantee optimal planning across many tools; it only makes each interaction's semantics available to that planner. Some environments remain too irreversible or opaque for model-directed execution. There, the correct architecture may be simulation, human approval, dual control or no automated write path.

These limits provide falsifiers. The contract thesis weakens in a domain where schema-only agents match stateful agents on held-out changes, ambiguous commits and missing-information cases, with the same verified outcomes and duplicate-effect rate. Conversely, a gain that appears only in format accuracy would not support the thesis.

A decisive evaluation should therefore hold the schema, model and user intent constant while changing only the hidden state or response condition. Score the final authorised state, the number of unintended effects, the recovery decision and the evidence retained for review. Report rates over all eligible episodes, not just calls the agent attempted. This design prevents abstention from masquerading as safety and prevents successful formatting from masking a duplicated or missing effect. It also reveals whether the contract changed behaviour under the very interventions its semantics were meant to govern.

How the claim changes under pressureChart · state
stateDiagram-v2
  [*] --> Claim
  state "The interface is a four-layer<br/>contract" as Claim
  state "Thought experiment one: hold<br/>the JSON fixed" as Test
  state "One valid call, two different<br/>worlds" as Revision
  Claim --> Test: expose mechanism
  Test --> Decision: evidence survives
  Test --> Revision: rival survives
  Revision --> Claim: narrow or rebuild
  Decision --> [*]
The revision loop begins with The interface is a four-layer contract and keeps counterexamples inside the reasoning path. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Compact glossary

Applicability
Whether a structurally valid action is suitable for the current world state and intent.
Effect receipt
Evidence from the executing system that a particular command produced a recorded effect.
Idempotency key
A stable request identifier that lets a service recognise and de-duplicate the same intended request.
Readback
An independent query of authoritative state or receipt after execution, especially after an ambiguous response.
Typed action contract
A versioned description of request shape, applicability, transition, errors and verification semantics.
Unknown outcome
A state in which the caller lacks evidence about whether an attempted effect committed.

Admit tools as state transitions

The practical decision changes at the registry boundary. Do not approve a tool because the model can name it, fill its JSON and pass a happy-path demonstration. Approve an interaction only when its current preconditions are observable, its effects are stated, its failures have next-action semantics and its consequential outcomes can be read back.

Then train on episodes that make those distinctions causal. Place the model where intent determines whether to call, ask, refresh, verify or stop. Place deterministic control where identity, permission, version, request de-duplication and hard limits can be enforced. Score the resulting system on verified state transitions and repeated consistency, while retaining format accuracy as one component metric.

The durable test of learned tool use is not whether the agent can produce a call. It is whether the system can explain, constrain and recover the state change that the call attempts. That standard turns a catalogue of functions into an operationally meaningful action space.

The evidence conversationChart · sequence
sequenceDiagram
  participant Q as Question
  participant M as Mechanism
  participant E as Evidence
  participant D as Decision
  Q->>M: The refund that succeeded twice
  M->>E: The interface is a four-layer contract
  E-->>M: compare rival explanations
  E->>D: Sources
  D-->>Q: revise scope or act
Question, mechanism, evidence and decision exchange the burdens developed in Tool Use as Learned API Interaction. Reading use: follow the relation, then test it against the surrounding evidence and limits.

Sources

  1. Primary research
    Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools, NeurIPS.
  2. Primary research
    Li et al., API-Bank: A Comprehensive Benchmark for Tool-Augmented LLMs, EMNLP.
  3. Primary research
    Patil et al., Gorilla: Large Language Model Connected with Massive APIs, NeurIPS.
  4. Primary research
    Qin et al., ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs.
  5. Primary research
    Hsieh et al., Tool Documentation Enables Zero-Shot Tool-Usage with Large Language Models.
  6. Primary research
    Patil et al., The Berkeley Function Calling Leaderboard: From Tool Use to Agentic Evaluation of Large Language Models, ICML.
  7. Primary research
    Yao et al., τ-bench: A Benchmark for Tool-Agent-User Interaction in Real-World Domains.
  8. Primary research
    Lu et al., ToolSandbox: A Stateful, Conversational, Interactive Evaluation Benchmark for LLM Tool Use Capabilities, Findings of NAACL.
  9. Official specification
    OpenAPI Initiative, OpenAPI Specification 3.2.0.
  10. Official specification
    OpenAPI Initiative, Arazzo Specification 1.1.0.
  11. Official specification
    JSON Schema, Specification and Validation Vocabulary 2020-12.
  12. Internet standard
    IETF, RFC 9110: HTTP Semantics, especially safe and idempotent methods and conditional requests.
  13. Internet standard
    IETF, RFC 9457: Problem Details for HTTP APIs.
  14. Official engineering guidance
    Google, AIP-193: Errors.
  15. Official engineering guidance
    Google, AIP-155: Request Identification.
  16. Official engineering guidance
    Google, AIP-194: Automatic Retry Configuration.
  17. Official observability specification
    OpenTelemetry, Semantic Conventions for HTTP Spans.