Skip to content

VOL 13 / CH 12 / LESSON 03

12.3 Tools and Agents: Execution State Machines Controlled by Host Programs ​

Consider “find the highest-latency service and create a repair ticket.” A valid monitoring query can be followed by a timed-out ticket request. If the server created the ticket before the timeout, a blind retry creates a duplicate. Fluent narration does not settle which operation completed.

The host controls the execution loop: the model proposes, the application validates and authorizes, the executor acts, and bounded results return to the model. Deterministic code owns the state transitions and permissions around side effects.

Learning Objectives ​

  • Implement agents as bounded state machines;
  • Design typed tool schemas, authorization, and parameter validation;
  • Handle idempotency, retries, timeouts, and partial failures;
  • Distinguish between conversation history, work state, and long-term memory;
  • Evaluate tool selection, parameters, results, and side effects.

1. Models Propose Tool Calls ​

Tool calling responses typically include a tool name and arguments. Both are untrusted inputs: the model might invent tool names, omit fields, provide incorrect types, or copy prompt injection patterns directly into the arguments.

The tool registry must define:

  • Stable tool names, versions, and purposes;
  • JSON Schema or equivalent typed input/output;
  • Required fields, enumerations, length, numeric, and format constraints;
  • Read-only or side-effect classifications;
  • Timeout limits, rate limits, cost implications, and data sensitivity;
  • Required permissions for callers;
  • Retry and idempotency contracts.

Unknown fields must be rejected by default to prevent the model from passing unaudited parameters directly to the underlying SDK. Tool outputs are also untrusted and must be constrained in size, escaped for the relevant output renderer, and strictly separated between data and instructions.

2. Permissions Won't Expand Due to Model Suggestions ​

What an agent can do for a user is determined by the current principal and business policies, not by how firmly the prompt is written. The host application must validate each call before execution by checking:

  • The current user or service identity;
  • Tenant, resource scope, and operation context;
  • Data classification and usage restrictions;
  • Whether secondary approval or confirmation is required;
  • Whether budget, rate, or time window limits have been exceeded.

For externally visible or irreversible actions, validate the concrete target and impact against existing authorization. When additional approval is needed, show the parsed operation for review. Persist approval with its action and argument version so later model edits cannot silently change what was authorized.

Never pass database superuser credentials or production deployment tokens into the model's context. Instead, credentials should be held by a restricted executor and used only in response to authorized tool calls.

3. Expressing Loops with a State Machine ​

This Python-shaped orchestration fragment requires the application’s state store, model, policy, approval service, executor, sanitizer, and completion verifier. record_model_reply records the reply without incrementing steps; the loop does that explicitly. Fresh-run counters are shown; resumed runs must restore persisted counters and reservations.

python
def run_agent(task, principal, registry, limits):
    state = new_run(task=task, principal=principal, limits=limits)
    tool_calls = 0

    while state.steps < limits.max_steps:
        if state.deadline_exceeded() or state.cost_exceeded():
            return state.stop("budget_exhausted")

        state.steps += 1
        reply = model.respond(
            messages=state.visible_messages(),
            tools=registry.schemas_for(principal),
        )
        state.record_model_reply(reply)

        if reply.final_answer is not None:
            if not verifier.accepts(state, reply.final_answer):
                return state.stop("completion_unverified")
            return state.finish(reply.final_answer)

        calls = registry.parse_and_validate(reply.tool_calls)
        if not calls:
            return state.stop("invalid_or_empty_action")

        for call in dependency_order(calls):
            if state.deadline_exceeded() or state.cost_exceeded():
                return state.stop("budget_exhausted")
            if tool_calls >= limits.max_tool_calls:
                return state.stop("max_tool_calls_reached")
            tool_calls += 1
            policy.authorize(principal, call)
            approval.require_if_needed(call, principal)
            result = executor.execute(
                call,
                timeout=min(registry.timeout(call.name), state.remaining_seconds()),
                idempotency_key=state.idempotency_key(call),
            )
            state.record_tool_result(call, sanitize(result))

    return state.stop("max_steps_reached")

A real implementation must capture exceptions from the model, validator, and tools, and categorize them as recoverable, unrecoverable, or requiring manual intervention. Do not infinitely feed error messages back into the model for retry.

A check before execution cannot reserve an unknown amount of future cost. Strict spending limits need atomic reservation, per-call caps, and settlement afterward. Model calls also need deadlines; cancellation does not prove a remote side effect was rolled back.

4. Planning is Strategy, Not a Permission Mechanism ​

Some tasks are well-suited to model invocation one step at a time. Others require first generating a plan, then executing it step by step. Still others can be handled with fixed workflows, where the model only fills in local parameters. The more stable and high-risk the process, the more appropriate deterministic orchestration becomes.

Publicly exposing a model’s full internal “Thought” process is not a necessary feature of an Agent. The system can retain structured plans, decision summaries, tool traces, and verifiable evidence, without relying on free-form chain-of-thought text for audit trails.

Parallel tool invocations are only suitable for independent actions. If one invocation depends on the output of another, an explicit dependency graph must be established; otherwise, the model might generate non-existent IDs or proceed with failed results.

5. Side Effects Require Idempotency and Compensation ​

A network timeout does not mean the server failed to execute. If the client simply retries a "create ticket" request, it may inadvertently generate duplicate side effects.

Prioritize design with the following principles:

  • Derive an idempotency key from the run/action identity;
  • Have the server store the key along with the outcome of the operation;
  • On retry, return the previously recorded result instead of re-executing;
  • For non-idempotent APIs, first check the current state, or implement business-level deduplication keys;
  • Define compensation actions and human intervention points for multi-step transactions;
  • Separate the "request accepted" state from the "business operation completed" state.

Distributed systems typically cannot achieve universal exactly-once guarantees through a single HTTP call. Applications must instead handle at-least-once delivery and duplicate results at the business logic level.

A durable action ID must survive retries and process restarts. The server must atomically bind its idempotency key to the argument hash and result, rejecting key reuse with different arguments. Identical arguments can represent two intentional actions, so an argument hash alone is not an action identity; key retention also limits the guarantee.

6. Set Budgets for Each Path ​

At a minimum, configure the following:

  • max model steps;
  • max tool calls and individual tool invocation counts;
  • token, monetary, and wall-clock budgets;
  • output size per invocation and total output size;
  • query and data scan limits;
  • retry counts and backoff strategies;
  • an allowlist of accessible tools.

Termination conditions are not limited to "the model says it's done." They also include: the target has been verified, no valid actions remain, repeated states, budget exhaustion, user cancellation, and irrecoverable errors.

When detecting duplicate calls, compare normalized arguments with the associated state, do not rely solely on comparing natural language responses.

7. Do Not Mix Three States Together ​

Conversation History ​

Used to understand the current interaction. History that is too long incurs token costs and introduces contamination from outdated instructions; summaries are lossy compressions, and critical constraints should be preserved as structured fields.

Work State ​

Includes task graph, completed actions, artifact IDs, approvals, budget, and errors. It must be persisted and validated by the application, not relied upon by the model to "remember."

Long-Term Memory ​

When storing user preferences or facts across sessions, consent, source, validity period, correctability/deletability, and access control are required. Summaries generated by the model cannot become long-term facts without independent verification.

8. Error Recovery: Errors Must Be Explainable ​

Establish limited, well-defined recovery strategies for failures:

  • Schema error: Return specific field-level errors and allow at most a few correction attempts;
  • Auth denied: Terminate the operation, no workaround via alternative tools is permitted;
  • Rate limit or transient error: Apply backoff according to defined policy;
  • Timeout or unknown outcome: First query the operation’s current status before deciding whether to retry;
  • Partial success: Log completed sub-steps and either trigger compensation logic or request manual intervention;
  • Unsafe output: Isolate the content and halt further propagation to high-privilege tools.

Each recovery action must record the triggering error, the applied strategy, the version of the call, and the final outcome. Agent "figuring things out on its own" is not a recovery strategy.

9. Evaluate the Full Execution Trace ​

A correct final answer does not guarantee absence of extraneous queries, unauthorized actions, or redundant side effects. Agent evaluation must be layered:

  • Tool selection accuracy;
  • Argument schema and semantic correctness;
  • Authorization decision validity;
  • Task completion success;
  • Side-effect correctness and duplication rate;
  • Recovery success;
  • Execution steps, latency, token consumption, and cost;
  • Rate of unsafe action attempts;
  • Whether the trace provides sufficient information for post-hoc debugging.

The test suite should include scenarios such as unavailable tools, malformed output, timeout, insufficient permissions, prompt injection, entity name collisions, and unknown execution results. Real-world side effects should be simulated using sandboxed or fake tools, or isolated execution environments.

Common Misconceptions ​

  • Agent equals a thinking LLM: Product behavior is determined by a combination of model, tools, state machines, and policies, rather than the model itself alone.
  • Tool schema ensures safety: It only provides partial structural validation and cannot replace authorization checks or business-level constraints.
  • Retrying after timeout is sufficient: The original operation may have already succeeded; idempotency keys or state queries are required.
  • Putting all history back is equivalent to memory: Doing so increases cost, introduces data pollution, and poses privacy risks.
  • Setting max_steps prevents uncontrolled behavior: Additional controls (such as tool access, permissions, cost limits, data scope, and side effects) are still necessary.

Exercise ​

  1. Design a strict input schema, permissions, and an idempotency contract for "create ticket".
  2. Draw a state transition diagram showing the flow of tool calls from proposed to validated, authorized, approved, and executed.
  3. Write a recovery procedure for timeouts with unknown outcomes.
  4. Distinguish between conversation history, work state, and long-term memory within a single task.
  5. Construct five agent test cases that expose repeated calls or permission circumvention.

Summary ​

An agent's core isn't giving models more autonomy, but rather placing uncertain action suggestions into a controlled execution loop. Typed schemas, per-call authorization, explicit state, idempotency, budgeting, and trajectory evaluation collectively constrain risk; the host application remains ultimately responsible for actual side effects.

The next lesson defines tasks, evidence, traces, statistical units, and release thresholds for evaluating this loop.

Built with VitePress | Software Systems Atlas