Skip to content
Agent Engineering Lab
Foundations
00F ·2026-07-05 ·20 min

The Production Boundary

An LLM gets dangerous not when it speaks but when it acts. What every tool call really asks for, and the boundary that turns an agent into a system component you can trust.

A chatbot that gets something wrong is annoying. An agent that gets something wrong has already sent the email, deleted the record, or booked the flight.

Sections 0a through 0e taught you how an agent works: messages, tokens, tool calls, the loop, frameworks, MCP. Every tool call in that story, going back to 0b, looked like a JSON object: a name and some arguments. It is not just data. It is a request to exercise power over something outside the model.

This chapter is about what changes the moment that request is granted. It is not the mechanics of building a tool call, Section 0b already covered that. And it is not any single production concern done in depth, chapters 5, 6, 10, 11, 12, and 13 each own one of those. This is the mental model that sits between them: what a tool call actually asks for, what has to be true before you let it run, and the shape of what can still go wrong once you do. A set of checks you carry into the next tool you wire up, not a rehearsal of chapters still to come.

Tools are capabilities

Every tool schema from Section 0b has the same shape: a name, a description, a JSON object of arguments. Nothing in that shape tells you how dangerous the tool is. search_docs and delete_record look identical to a schema validator. The difference isn’t in the JSON. It’s in what happens on the other side of the call.

Tool typeExampleRisk
Read-onlysearch docs, check weatherlow
Internal readread a customer profile, search internal emailprivacy
Internal writecreate a ticket, update a recordbusiness
External side effectsend an email, call a partner API, book a meetingreputation
Privilegeddeploy, approve a payment, delete a recordhigh

The same agent loop from 0c is safe wired to the top row and dangerous wired to the bottom, and the code you’ve written so far treats every tool identically: validate the schema, execute, return the result. That’s the gap this chapter closes.

Five questions before every tool call

A tool call that passes schema validation has cleared exactly one bar. Before you let it run, five separate questions need an answer:

  1. Can the agent call this tool at all?
  2. Can it call it for this user?
  3. Can it call it on this resource?
  4. Can it call it with these arguments?
  5. Should a human approve first?

Five questions, one shape: actor, action, resource, scope, approval. Every access-control system you’ll ever wire into an agent is some arrangement of those five fields.

def before_tool_call(actor, tool, resource, args):
    # Schema validation already passed (chapter 00b). That is not enough.
    if not policy.allowed(actor, tool.name, resource, args):
        return ToolResult(error=f"{actor} may not call {tool.name} on {resource}")
    if tool.risk == "privileged":
        approval = require_human_approval(actor, tool, resource, args)
        if not approval.granted:
            return ToolResult(error="awaiting human approval")
    return None  # authorized: proceed to execute

A valid tool call, one that passed 00b’s schema check, can still be forbidden. Validation is not authorization.

Chapter 10 builds the policy engine policy.allowed stands in for here. Chapter 5 covers the human-approval step in depth: where it fits in the loop and how to design it so it isn’t just a rubber stamp.

A message list is not memory

The message array from Section 0c, the one messages.append() grows every turn, looks like memory because it persists across the run. It isn’t the same thing, and treating it as such is one of the more expensive mistakes in early agent systems. A production agent is carrying at least seven distinct kinds of state, and a message list is only one of them:

StateWhat it is
Prompt statewhat is in the current request
Conversation stateprior messages replayed into context
Scratchpad stateintermediate reasoning or notes
Tool stateresults returned from an API or database
Durable statepersisted memory, records, workflow state
User-visible statewhat the user can inspect
Hidden system statepolicies, secrets, credentials, internal traces

A message list is not memory. A vector database is not memory either, it’s a retrieval mechanism sitting in front of some other store. Durable state, the record that survives after the conversation and the process both end, is a product decision made deliberately. It is not a side effect of using an LLM with a big context window. Chapter 12 goes deep on how to design and govern it.

Agents must survive duplicate actions

An agent repeats a tool call for reasons that have nothing to do with malice: the first result was ambiguous, so it tries again. A network call times out and retry logic fires. A user double-taps a button that maps to the same request twice. None of this is unusual, it’s the default behavior of any distributed system, and an agent calling external tools is a distributed system with an LLM making the retry decisions. A calculator tolerates being called twice with identical input. send_email, charge_card, and delete_record do not.

ConceptWhy it matters
Timeout budgeta slow tool must not hang the whole loop
Retrytransient failures are normal
Idempotency keya repeated call must not repeat the side effect
Cancellationthe user may stop the task
Compensationa partial side effect may need undoing
Dry runshow the intended action before doing it
def call_tool_once(tool, args, *, user_id):
    key = idempotency_key(user_id, tool.name, normalize(args))
    cached = results_store.get(key)
    if cached is not None:
        return cached  # repeated call, do not repeat the side effect
    result = run_with_timeout(tool, args, seconds=tool.timeout_budget)
    if tool.side_effecting:
        results_store.put(key, result)
    return result

If you cannot replay it, it is a demo

Every failure mode in this chapter eventually reaches someone asking what actually happened. Answering that needs more than server logs or a print statement. It needs a trace: a structured record of the run.

{
  "run_id": "run_123", "user_id": "u_456", "agent": "support_agent",
  "model": "the model under test", "input": "the user request",
  "tool_calls": [
    {"tool": "search_docs", "arguments": {"q": "refund policy"},
     "authorized": true, "latency_ms": 280, "result_summary": "3 documents"}
  ],
  "final_answer": "the response returned to the user",
  "cost": {"input_tokens": 4210, "output_tokens": 780},
  "stop_reason": "completed"
}

If you cannot replay, inspect, and evaluate a run from its trace, you do not have a production agent. You have a demo that happened to work the last time someone was watching. This is the same trace shape Chapter 6’s evaluation suites score, field for field.

A failure taxonomy

Ten kinds of failure, not one. Knowing which one you’re looking at determines the fix, which is why this table is worth returning to any time an agent misbehaves in a way you haven’t diagnosed before:

FailureExampleMitigation
Modelhallucinated answergrounding, evals
Formatinvalid JSON from the modelschema validation, retry
Tool selectionwrong tool chosenbetter tool descriptions, evals
Tool executionAPI timeout or errortimeout, retry, fallback
Authorizationagent attempts a forbidden actionpolicy gate
Statestale context drives a wrong stepstate refresh
Memorya wrong remembered factmemory governance
Looprepeats the same stepstep budget, repetition detection
Securityprompt injection via tool outputinput isolation, tool policy
Productuser cannot trust the resulttrace, explanation, handoff

Model and format failures are 0a and 0b’s territory, tool selection and execution are 0c’s. The rest, authorization, state, memory, loop, security, product, are what the sections above named and what chapters 5 through 13 build the real defenses for.

How you know the boundary holds

Every claim made above, that a tool call was authorized, that a retry was idempotent, that a trace is complete, is testable, and none of it is verified by code review alone. Four layers of evaluation catch what review misses:

  • Unit tests for tools. Does the tool do what its schema promises, on valid input and on the edge cases.
  • Scenario tests for loops. Does the full agent, run end to end on a realistic task, make the right sequence of calls and land on the right answer.
  • Regression tests for prompts. Does a change to the system prompt or the model version break a case that used to pass.
  • Adversarial tests for injection. Does the agent hold its authorization boundary when a tool result tries to talk it into something else.

This is the same eval habit flagged by the eval callouts placed throughout the rest of the book’s chapters. Chapter 6 builds the harness that runs all four layers against a real agent, with the metrics that tell you whether a change made things better or worse.

Where this goes

Six chapters build out pieces of this boundary in depth. Chapter 5 covers human-in-the-loop as architecture: where approval fits in the loop and how to keep it from becoming theater. Chapter 6 builds the evaluation and hardening harness that scores the trace from the section above. Chapter 10 covers governance and auditability: the policy engine behind before_tool_call, and what an audit needs beyond a log line. Chapter 11 is the security deep dive: prompt injection through tool output, the confused-deputy problem, and what changes when the tool itself is compromised, not just the model. Chapter 12 covers memory management: designing the durable-state row from the state section as an actual product decision instead of an accident of context-window size. Chapter 13 covers agent protocols in production, including what a protocol like MCP leaves unsolved: who is calling, on whose authority, and how you prove it after the fact.

That last question, who is allowed to act and how you prove it after the fact, is the subject of the author’s ongoing work on agent identity and authorization, including the piece “Who Authorized That?”, and readers who want to follow it further can start at sunilprakash.com/writing.