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
- AI Agents Part 1: Architecture
- AI Agents Part 2: Harness and CLI
- AI Agents Part 3: Tools and APIs
- AI Agents Part 4: MCP
- AI Agents Part 5: Context and Memory
- AI Agents Part 6: Agentic RAG
- AI Agents Part 7: Evals and Observability
- AI Agents Part 8: Security and Deployment
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.
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.
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.
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.
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.