An AI agent is ordinary software that lets a model choose some of its next steps. The architecture question is where model judgment stops and deterministic code takes over. The model should interpret language and pick among allowed actions. Code should own identity, permissions, money, writes, budgets, and the decision to stop. This part explains that split, the task contract that records it, and the model boundary that enforces it.

AI Agent Engineering Series

What an AI agent is

An AI agent is an application in which a model chooses at least part of the sequence of actions. The application still receives requests, stores state, calls services, checks permissions, and records events. The model is one component inside it. Ordinary code controls which actions the model can request and what happens to each request.

Anthropic's engineering guide Building Effective Agents draws the line between the two main designs. The guide defines workflows as "systems where LLMs and tools are orchestrated through predefined code paths". It defines agents as systems where "LLMs dynamically direct their own processes and tool usage". The ReAct paper (Yao et al.) described the loop that most agents still use. The model alternates between reasoning about the task and taking actions that return new information.

The useful question for an architect is who chooses each arrow in the run. Figure 1 shows the three designs by that test.

Single call, workflow, and agent compared by who chooses each stepA single model call maps a request to a result in one step. A workflow runs a fixed chain of steps, and code chooses every arrow, even when one step calls a model. An agent alternates between model calls and tool calls, and the model chooses which tool to call next and when to answer. The more arrows the model chooses, the more possible runs the team must test. Single call Request Model Result One input, one output, no sequence Workflow Request Extract Look up Check Result Code fixes every branch before the run Agent Request Model Tool Model Result picks the next tool Model picks some arrows, so many paths to test model chooses code chooses model call
FIG 01A workflow can call a model at any step, but only an agent lets the model choose the next step.

Choose the simplest design that works

The three designs solve different problems, and one product often uses all three.

A single model call maps one input to one output. Classification, extraction, rewriting, and structured analysis often fit here. The application calls the model once and continues in code.

A workflow follows a path that the developer chose in advance. It can call models at several steps, but code decides the sequence. It fits when the steps are known and the main work is to handle data at each step.

An agent lets the model choose part of the sequence. It can pick an allowed tool, read the result, and pick the next step from what it learned. It fits when the path depends on facts that only appear during the run.

Anthropic's guide recommends "finding the simplest solution possible, and only increasing complexity when needed". It warns that agentic systems trade latency and cost for better task performance. It also names "the potential for compounding errors" as a cost of autonomy.

Question about the task If yes If no
Is one response enough to finish the task? Single model call Workflow or agent
Can you list every step and branch before the run starts? Workflow Agent
Does the next step depend on results that you cannot predict? Agent Workflow
Is a wrong step expensive or hard to undo? Keep that step in code or behind approval Model choice may be acceptable
Can code check success without the model? Good agent candidate Build that check first

The cost of autonomy grows with each model-chosen step. With 3 allowed tools and 4 model-chosen calls, the model can choose 3 x 3 x 3 x 3 = 81 different call sequences. A workflow fixes its branches in code, so its tests can cover each one. For an agent, test the safety rules that must hold on every path, plus representative paths with failures, retries, and unexpected tool choices. Our post on compound error and cost in agents works through how per-step error rates multiply across a run.

A mixed design is often the easiest to control. A support system can classify each request with one model call and run a fixed identity check in code. Only the unusual cases go into an agent loop. Our comparison of agentic workflows and traditional automation gives more examples of this split.

The task contract

A usable task statement is "read a duplicate-charge claim, check the invoice, and prepare a refund for approval". It names the input, the result, the records that the system may read, and the action that needs a person.

A task contract is a short, typed record of that specification. People write it, and code enforces it. Write it before any prompt. A prompt can only ask the model to stay in scope. Code that reads the contract can refuse a request that is out of scope.

Field Question it answers Example
Goal What does a successful run produce? A refund proposal ready for approval
Inputs Which verified facts start the run? Invoice ID and customer ID from the session
Allowed reads Which tools may observe data? read_invoice
Allowed writes Which tools may change data? propose_refund
Stop conditions When is the run finished? A proposal exists, or the claim is not supported
Handoff conditions When must a person take over? Refund above 500 USD, or the customer disputes the policy

Three rules make a contract useful.

Reads and writes are separate lists. A wrong read can expose data that the user may not see. A wrong write can also move money or send a message that nobody can take back. With two lists, the run loop can send every write to an approval step without asking the model whether the action is risky. A tool that appears on both lists is a design error. Our guide to writing down an agent's allowed actions covers how to draw this line for business processes.

Every contract has at least one stop condition. If you cannot say how a run ends, you cannot test it. The agent can then loop until a budget stops it. Prefer conditions that a system of record can confirm, such as "a proposal row exists". Our post on verifiable outcomes explains why outcomes that a system can check make better agent targets.

The contract is frozen for the run. No step can add a tool to its own contract. Unknown fields fail loudly. A typo such as "allowed_write" must stop the run, because a contract that silently has no writes produces a confused agent and no error.

Split the work between the model and code

A deterministic rule gives the same result for the same verified input. Use code for these rules. Use the model where the system must interpret unclear language or compare uncertain evidence.

Keep in deterministic code Give to the model
Authentication and authorization What the user wants from an unclear request
Exact calculations, such as refund amounts Which allowed tool answers the current question
Schema checks and allowlists Whether the evidence so far is enough
State transitions and stop rules How to explain a result to a person
Step, token, and spend limits
Writes, approvals, idempotency, audit records

The OWASP entry on Excessive Agency names three root causes of damaging agent actions: excessive functionality, excessive permissions, and excessive autonomy. One mitigation is to "Implement authorization in downstream systems rather than relying on an LLM to decide if an action is allowed or not".

Follow one request through the split. A customer writes: "I was charged twice for invoice 456. Please refund the extra payment." Figure 2 shows which side owns each step.

A duplicate-charge refund split between model and codeCode reads the customer from the authenticated session. The model asks to call read_invoice, and code checks that the tool is in the contract and that the customer owns the invoice. The invoice service returns two payments of 120 USD. The model asks to refund the second payment. Code computes the amount and applies the approval rule, the run waits for a person, and code commits the refund once and stores the receipt. The model only proposes. Every step that touches identity, money, or a write is on the code side. Model proposes Code decides and acts Read customer from session never from model output Ask for read_invoice picks an allowed read Check tool and owner contract + authorization Invoice service replies two payments of 120 USD Ask to refund pay_2 judges the evidence Compute amount apply the approval rule Person approves the run waits Commit once idempotency key, receipt Each model proposal crosses the dashed line into a check in code.
FIG 02The model makes two proposals, and code owns the other six steps.

The model never chooses the customer, calculates money, or approves the refund. It never declares that the payment succeeded, because only the payment service can confirm a write. It interprets the request, picks a relevant read, and judges whether the evidence supports a proposal. Each later part of the series adds machinery and keeps this division.

If a decision in your task has no clear owner, keep that capability out of the first release.

The model boundary

The model boundary is the point where a model response becomes something that the application may act on. Everything on the model side is untrusted text. Everything on the application side is a typed value that passed validation.

A response that passes becomes one of three proposals:

  • tool_call: run one named tool from the contract with these arguments.
  • final: the task is done, and here is the answer.
  • handoff: stop and give the task to a person, with a reason.

Three actions are enough for the whole series. Every later feature, such as approvals, retrieval, or MCP tools, arrives as a tool call or a handoff. A small, closed set of actions keeps every branch of the run loop testable.

The model boundary turns an untrusted response into a typed proposalA provider response is untrusted text. A provider adapter, the only code that knows the provider format, converts it to a plain internal record. The boundary then runs four checks: one object with no unknown fields, an action from the closed set, a tool that is in this run's contract, and no tool on a final or handoff. A failed check produces a rejection with a stable error code. A response that passes becomes one of three typed proposals: tool_call, final, or handoff. The run loop still checks budgets and approvals after this point. Provider response untrusted text, JSON, tool calls Provider adapter the only code that knows the provider Boundary checks 1. One object, no unknown fields 2. Action is tool_call, final, or handoff 3. Tool is in this run's contract 4. Final and handoff name no tool fail Reject stable code, such as unknown_field pass tool_call run one allowed tool final answer and stop handoff give it to a person Each outcome is a proposal. Budgets and approvals come next.
FIG 03Only a response that passes all four checks becomes something the application may act on.

Unknown fields are errors. Reject any field outside the known set with the code unknown_field. If a model starts to return "confidence" or "approved: true", you want a visible rejection. A parser that ignores extra keys lets a field through that some later code might trust.

Rejections carry stable codes. A code such as tool_not_allowed lets traces and tests count failures by type. A run with 40 rejections of one code points to a prompt or tool-description problem. Part 7 turns those counts into eval metrics.

The tool check runs against this run's contract. A model can only request a tool, and a request outside the contract fails with tool_not_allowed. That request never reaches a handler, even if the tool exists elsewhere in the system. Part 3 adds argument checks against each tool's input schema.

Final and handoff cannot name a tool. This removes an ambiguous case, a response that says "done" and also requests a write.

The arguments are copied. The decision holds its own copy, so no later mutation of the raw response can change what the run executes.

A cut-off response is a handoff. Claude's tool use documentation describes the shape of a tool request. The response has stop_reason "tool_use" and a tool_use block with an id, a name, and an input. A response that ends on max_tokens or a refusal can hold a partial tool input. The adapter maps both cases to a handoff, because a partial write request is never safe to run.

One adapter per provider. Only the adapter imports a provider SDK. The rest of the application sees the internal record. A second provider then means one new adapter and the same boundary tests.

Provider features can narrow the shape of a response, and they do not replace the boundary. Anthropic's structured outputs documentation says strict tool use will "guarantee schema validation on tool names and inputs." That guarantee covers the shape of the call. It does not check the permissions of this run, and another provider might not offer it.

Test the boundary without a model

The boundary is plain code, so you can test it with no model at all. A fake model is a test double that returns scripted responses in order. It has no randomness, no network, and no cost.

Script a good path, a request for a tool outside the contract, a response with an extra field, and a cut-off response. Then assert that the boundary accepts or rejects each one with the expected code. Each time a trace shows a new bad response, add it to the script. The test suite then becomes a list of the ways that models fail at your boundary, and each failure stays fixed.

Testing without a model also controls cost. Boundary tests run in milliseconds on every commit. Live model runs belong in evals, which Part 7 covers, where you measure behavior across many trials.

What validation does not prove

Schema validation checks the shape of the response. It does not prove that the action is appropriate, that the evidence is sufficient, or that the user has permission. Each of those needs its own control.

Control What it catches What it cannot prove Where
Boundary shape checks Missing fields, wrong types, unknown fields Good judgment Part 1
Contract allowlist Tools outside the task The right tool choice Part 1
Step and spend budgets Runaway loops Task success Part 2
Authorization in the tool Access to another customer's data Factual accuracy Parts 3 and 8
Business rules in code A wrong amount or an invalid state change A clear explanation Part 3
Evals on repeated runs Recurring behavior failures Permission to act Part 7

Deterministic code can still contain bugs. The term means that the same verified input follows the same programmed rule. That makes the result reproducible and testable. It does not make the rule correct, so the rule needs tests too.

Model output also varies with sampling, model updates, and the context that each call receives. The boundary does not remove this variation. It controls how the application responds to it. Part 7 measures the variation with repeated-run evals.

Four layers, and state kept apart from context

A practical agent has four layers. Each layer talks to the next through a small record, such as a typed decision or a tool request. One large conversation object that every layer reads and writes makes failures impossible to locate.

Four layers of an agent, with run state kept apart from model contextThe model boundary turns responses into typed decisions. The control loop owns run state, budgets, stop rules, and the trace. The capability layer holds narrow read and write tools that return receipts. The policy and evidence layer supplies context, memory, retrieval, identity, and approvals. Each layer passes a small record to the next. The control loop owns the full run state, which includes service tokens. For each model call it selects a small slice, the request, the evidence, and the allowed tools, as the model context. Tokens and most state never enter the context. Model boundary Part 1 response to typed decision decision Control loop Part 2 state, budgets, stop rules, trace tool request Capability layer Parts 3-4 narrow reads and writes, receipts access check Policy and evidence Parts 5-8 context, memory, approvals Model context request, evidence, allowed tools selects Run state run ID, status budget left verified IDs tool receipts approvals service tokens stay out of context
FIG 04The control loop owns all run state and shows the model only the slice that one decision needs.

The control loop is the subject of Part 2. The capability layer is Parts 3 and 4, and the policy and evidence layer is Parts 5 to 8.

Give each tool one narrow operation. A shell, a database connection, or a general HTTP client gives the model every operation that the credential allows. The contract can then no longer describe what the run may do.

Store run state outside the conversation transcript. State is the set of application facts that control the run. Examples are the run ID, the status, verified resource IDs, completed tool calls, the remaining budget, and approvals. Context is the information that the model sees for one decision. The complete state should rarely enter context. A tool adapter may need a payment-service token, but the model never does. Part 5 covers how to select context for each step.

Explicit state also makes failures easy to locate. "The answer was wrong" does not name a component. A trace that shows a successful lookup, a missing policy source, and an answer with no evidence names three.

Framework or from scratch

You do not need a framework to build an AI agent. The core loop is a small amount of ordinary code, and Part 2 shows its shape. Anthropic's guide recommends starting with model APIs directly. It warns that frameworks "often create extra layers of abstraction that can obscure the underlying prompts and responses, making them harder to debug."

Use a framework when Write the loop yourself when
It removes work that your team already understands You still need to learn where the failures come from
You can see the exact prompt and tool list for each call The framework hides prompts, retries, or state
Its state and stop rules map to your contract Your approval or audit rules do not fit its model
You can replace it without rewriting your tools It owns your tool definitions and credentials

Our guide on choosing an agent platform without lock-in lists what to check before you adopt one.

How OpenNash helps

OpenNash puts forward-deployed engineers on your team to build production agents with this boundary in place. We work with Fortune 2000 companies. We write the task contract with your team for one workflow. We build the boundary, the loop, and the tools in your codebase. We add tests and evals from your own requests before the first release. You own the code, the contracts, and the eval sets. If your task fits a fixed workflow, we tell you so.

Book a call with an OpenNash forward-deployed engineer or email us at hello@opennash.com. In the first session, we review your candidate workflows and show which ones deliver the highest business value and the fastest ROI.

Next: AI Agents Part 2: Harness and CLI explains the run loop around this boundary. It covers state as an event log, budgets that always stop the run, and replay.