An AI agent is not simply an LLM with a long system prompt. It is a system in which a model can choose actions, observe results, update state, and continue until it reaches a stop condition. The model supplies judgment; the surrounding runtime supplies tools, permissions, memory, recovery, and evidence that the work was done correctly.
This distinction matters. Most failed agent projects do not fail because the model cannot reason. They fail because the system gives an uncertain model broad permissions, ambiguous tools, no durable state, and no reliable way to verify outcomes.
This guide develops a production-oriented architecture from first principles, then implements a small working Agent runtime in Go.
Key Takeaways
- Use a deterministic workflow when the path is known; use an agent only when the model must choose the path.
- Treat the agent loop as a state machine with explicit budgets and stop conditions.
- Design tools as narrow, typed capabilities. Tool output is untrusted input, even when it comes from your own infrastructure.
- Separate working state, conversation history, and long-term memory.
- Require human approval before consequential or irreversible actions.
- Evaluate trajectories and business outcomes, not just the final prose response.
- Start with one agent. Add specialists only when isolation, ownership, or parallel work creates measurable value.
What Is an AI Agent?
Anthropic makes a useful architectural distinction: a workflow follows code-defined paths, while an agent lets the model dynamically direct its process and tool use. Both are agentic systems, but their operating risk is different.
| System | Who selects the next step? | Best fit | Main advantage |
|---|---|---|---|
| Single LLM call | Application code | Classification, extraction, rewriting | Lowest cost and latency |
| Deterministic workflow | Application code | Stable business processes | Predictable and testable |
| Agent | Model within policy boundaries | Open-ended investigation and execution | Adapts to uncertain paths |
An agent is justified when all three conditions hold:
- The task requires several steps or tools.
- The correct sequence cannot be fully known in advance.
- The environment provides feedback that lets the agent verify progress.
If a request can be solved by one model call, retrieval plus one model call, or a fixed directed acyclic graph, adding an agent usually increases latency, cost, and failure modes without increasing user value. Anthropic's guidance is explicit on this point: begin with the simplest solution and accept agentic complexity only when the performance benefit warrants the trade-off.
The Production Agent Architecture
A useful mental model is:
agent = model + harness + tools + environment
The model interprets the goal and chooses a next action. The harness owns instructions, state transitions, budgets, guardrails, and error handling. Tools expose bounded capabilities. The environment determines which files, networks, credentials, and services the process can access.
This is also the practical meaning of bounded autonomy: the model may choose among allowed actions, but it cannot expand its own identity, permissions, budget, network, or definition of success. AWS's Agentic AI Lens treats those boundaries, traceability, evaluation, and human oversight as architecture concerns rather than prompt-writing advice.
The loop is simple. Production behavior is not. Each arrow is a contract that needs a schema, timeout, ownership, and failure policy.
1. Goal and Policy
Translate a user request into an explicit execution contract:
- desired outcome;
- allowed and forbidden actions;
- data boundaries;
- cost and time budget;
- actions that require approval;
- definition of done.
"Resolve the customer's refund issue" is too vague. A safer contract is: "Inspect order and payment status, explain eligibility, and draft a refund. Never submit a refund above a policy-configured threshold or alter an order without approval." The threshold is an example policy value, not a universal business rule.
Prompt instructions are not an authorization system. Enforce permissions in application code and at the tool boundary.
2. Context and State
Context is the information presented to the model for its next decision. State is the authoritative record maintained by the runtime. Do not treat a growing chat transcript as both.
| State type | Scope | Example | Storage rule |
|---|---|---|---|
| Working state | Current run | order ID, completed steps, retry count | Checkpoint after meaningful transitions |
| Conversation state | User thread | clarifications and prior answers | Retain only as long as product needs |
| Long-term memory | Across threads | stable user preference | Write selectively with provenance |
| Business state | Source system | payment and shipment status | Read from the system of record |
Retrieval should be deliberate. Load the smallest context that supports the next decision, attach source metadata, and prefer fresh reads for mutable business facts. A vector database is useful for semantic recall, but it must not become a shadow source of truth for balances, permissions, or order status.
3. Tools
A tool is a capability boundary, not a convenience wrapper around an internal API. Good tools make the correct action easy and dangerous actions difficult.
package refund
import (
"errors"
"regexp"
)
var orderIDPattern = regexp.MustCompile(`^ord_[a-zA-Z0-9]+$`)
type Draft struct {
OrderID string
AmountCents int64
Reason string
}
func (draft Draft) Validate(maxAmountCents int64) error {
if !orderIDPattern.MatchString(draft.OrderID) {
return errors.New("invalid order ID")
}
if draft.AmountCents <= 0 || draft.AmountCents > maxAmountCents {
return errors.New("refund amount is outside policy")
}
switch draft.Reason {
case "duplicate", "damaged", "not_received":
return nil
default:
return errors.New("unsupported refund reason")
}
}
Apply these rules:
- Give each tool one clear responsibility.
- Use typed inputs and structured outputs.
- Describe side effects and failure modes.
- Enforce identity, authorization, and tenant isolation inside the tool.
- Add timeouts, bounded retries, and idempotency keys.
- Return concise evidence, not entire database rows or unbounded logs.
- Treat tool descriptions and tool results as untrusted data.
The MCP specification makes the same security point for interoperable tools: users should understand exposed capabilities and retain control over sensitive invocations. MCP standardizes discovery and transport; it does not remove the need for application-level authorization.
4. The Agent Loop
The runtime repeats four operations:
- Build context from the goal and current state.
- Ask the model for a tool call, a final answer, or an escalation.
- Execute the permitted action and capture the observation.
- Update state and test a stop condition.
Every loop needs hard limits:
const (
maxTurns = 12
maxToolCalls = 20
maxWallSeconds = 90
maxCostMicros = 500_000
)
These limits are illustrative starting points. Represent money as an integer unit instead of a floating-point value. Derive the actual limits from workload latency, provider pricing, retry behavior, and the business impact of interruption, then enforce them in the runtime rather than only describing them in a prompt.
Stop when the outcome is verified, the user must decide, the agent repeats without progress, or a budget is exhausted. "Keep trying until it works" is not a production policy.
5. Guardrails and Human Approval
Guardrails belong at several layers:
- Input: detect unsupported requests, prompt injection, and sensitive data.
- Model decision: restrict available tools based on user and task context.
- Tool input: validate schema, authorization, and business rules.
- Tool output: sanitize untrusted content and cap response size.
- Final output: verify required fields, citations, and policy compliance.
Use human approval for actions such as sending messages, moving money, deleting data, publishing changes, or granting access. The approval screen should show the exact proposed action and parameters, not a generic "allow agent" button.
Runtime enforcement is the important word. The OWASP Agent Control Standard describes framework-independent middleware hooks for inspecting and controlling Agent behavior at runtime. Whether or not a team adopts that standard, the architecture should expose equivalent interception points before model calls, tool dispatch, external writes, memory updates, and final delivery.
6. Observability and Evaluation
Traditional service metrics remain necessary: latency, error rate, saturation, and cost. Agents also need semantic and trajectory-level evidence:
- model request and response metadata;
- selected tool and validated arguments;
- tool result, duration, and error category;
- state transition and retry reason;
- approval event and actor;
- final outcome and verification result.
Do not log secrets or unrestricted customer data. Redact before export and define retention separately for traces and product records.
| Level | Question | Example metric |
|---|---|---|
| Outcome | Did the user goal succeed? | Correct resolution rate |
| Trajectory | Did the agent take a sound path? | Invalid calls, loops, unnecessary steps |
| Operations | Was it efficient and reliable? | p95 latency, cost, escalation rate |
Build an evaluation set from real, anonymized tasks: normal cases, ambiguous requests, tool failures, permission violations, prompt injection, and incomplete data. Run it on every prompt, model, tool-schema, or orchestration change. A final answer can sound excellent while the underlying action is wrong; outcome verification is therefore the release gate.
Release the Whole Agent, Not Only the Model
An Agent's production behavior is the interaction of several changing parts. Treat them as one versioned release unit:
| Release component | Evidence required before rollout |
|---|---|
| Model and provider settings | pinned identifier, fallback route, usage and latency comparison |
| Instructions and context policy | version, red-team cases, context and data-boundary tests |
| Tool schema and implementation | contract tests, authorization tests, side-effect and idempotency tests |
| Runtime policy and identity | principal source, delegated scopes, approval rules, expiry and revocation |
| State and memory | schema migration, provenance, retention, deletion and rollback |
| Evaluators and datasets | evaluator version, slice coverage, known limitations and adjudication |
Use a manifest to connect those versions to a deployment. Run offline regression and abuse suites, canary the complete manifest, compare business outcomes by risk slice, and keep a tested rollback path. A model upgrade with an old tool schema can fail even when each component passed an isolated test.
Identity is a runtime boundary, not context supplied by the model. Workload identity authenticates the Agent service; delegated user identity constrains which object and action it may access. Issue short-lived, audience-bound credentials after policy evaluation, pass them directly to the relevant executor, and never ask the model to copy bearer tokens between tools. NIST's AI Agent Standards Initiative, announced in February 2026, identifies agent identity and authorization as active standards work; it is an initiative and research program, not a finished universal certification.
A Runnable Go Agent Runtime
The most durable way to learn Agent architecture is to make the runtime contract visible. The following program uses only the Go standard library. Its scriptedModel is a deterministic model adapter used as a test double, so the authorization and stop behavior can run offline. Replace that adapter with a provider client that returns the same typed decision; keep policy, tool execution, budgets, and verification in the runtime.
Create support_agent.go:
package main
import (
"context"
"errors"
"fmt"
)
type actionKind string
const (
callTool actionKind = "call_tool"
finish actionKind = "finish"
)
type decision struct {
Kind actionKind
Tool string
OrderID string
Final *finalAnswer
}
type finalAnswer struct {
OrderID string
Status string
Refundable bool
}
type observation struct {
Code string
OrderID string
Status string
Refundable bool
}
type runState struct {
Goal string
Turns int
ToolCalls int
Observations []observation
Result string
}
type model interface {
Decide(context.Context, runState) (decision, error)
}
type scriptedModel struct{}
func (scriptedModel) Decide(_ context.Context, state runState) (decision, error) {
if len(state.Observations) == 0 {
return decision{Kind: callTool, Tool: "get_order_status", OrderID: "ord_1001"}, nil
}
current := state.Observations[len(state.Observations)-1]
if current.Code != "ok" {
return decision{}, errors.New("cannot produce a verified answer")
}
return decision{
Kind: finish,
Final: &finalAnswer{
OrderID: current.OrderID,
Status: current.Status,
Refundable: current.Refundable,
},
}, nil
}
type authContext struct {
TenantID string
PrincipalID string
}
type order struct {
ID string
TenantID string
Status string
Refundable bool
}
type runtime struct {
Model model
Orders map[string]order
MaxTurns int
MaxToolCalls int
Checkpoint func(runState) error
}
func (agent runtime) Run(
ctx context.Context,
auth authContext,
goal string,
) (runState, error) {
state := runState{Goal: goal}
for state.Turns < agent.MaxTurns {
if err := ctx.Err(); err != nil {
return state, err
}
next, err := agent.Model.Decide(ctx, state)
if err != nil {
return state, err
}
state.Turns++
switch next.Kind {
case callTool:
if state.ToolCalls >= agent.MaxToolCalls {
return state, errors.New("tool-call budget exhausted")
}
if next.Tool != "get_order_status" {
return state, errors.New("tool is not allowlisted")
}
result, err := agent.getOrderStatus(auth, next.OrderID)
if err != nil {
return state, err
}
state.ToolCalls++
state.Observations = append(state.Observations, result)
case finish:
if len(state.Observations) == 0 || next.Final == nil {
return state, errors.New("final answer has no supporting observation")
}
current := state.Observations[len(state.Observations)-1]
if next.Final.OrderID != current.OrderID ||
next.Final.Status != current.Status ||
next.Final.Refundable != current.Refundable {
return state, errors.New("final answer contradicts tool evidence")
}
state.Result = fmt.Sprintf(
"Order %s is %s; refundable=%t.",
next.Final.OrderID,
next.Final.Status,
next.Final.Refundable,
)
if err := agent.checkpoint(state); err != nil {
return state, fmt.Errorf("checkpoint final state: %w", err)
}
return state, nil
default:
return state, errors.New("unsupported model decision")
}
if err := agent.checkpoint(state); err != nil {
return state, fmt.Errorf("checkpoint transition: %w", err)
}
}
return state, errors.New("turn budget exhausted")
}
func (agent runtime) checkpoint(state runState) error {
if agent.Checkpoint == nil {
return nil
}
return agent.Checkpoint(state)
}
func (agent runtime) getOrderStatus(
auth authContext,
orderID string,
) (observation, error) {
if auth.PrincipalID == "" || auth.TenantID == "" {
return observation{}, errors.New("authenticated context is required")
}
item, ok := agent.Orders[orderID]
if !ok || item.TenantID != auth.TenantID {
return observation{}, errors.New("order not found or not authorized")
}
return observation{
Code: "ok",
OrderID: item.ID,
Status: item.Status,
Refundable: item.Refundable,
}, nil
}
func main() {
agent := runtime{
Model: scriptedModel{},
Orders: map[string]order{
"ord_1001": {
ID: "ord_1001",
TenantID: "tenant-a",
Status: "delivered",
Refundable: true,
},
},
MaxTurns: 6,
MaxToolCalls: 2,
Checkpoint: func(state runState) error {
fmt.Printf("checkpoint turns=%d tools=%d\n", state.Turns, state.ToolCalls)
return nil
},
}
state, err := agent.Run(
context.Background(),
authContext{TenantID: "tenant-a", PrincipalID: "user-42"},
"Can order ord_1001 still be refunded?",
)
if err != nil {
panic(err)
}
fmt.Println(state.Result)
}
Run it:
go run support_agent.go
The example makes five boundaries observable: the model can emit only typed decisions; the runtime allowlists tools; the tool receives authenticated context outside model arguments; budgets stop unbounded execution; and the final structured result must match a fresh observation before the runtime renders prose. The checkpoint hook runs after meaningful transitions. A production implementation should write it atomically to durable storage and attach trace IDs, model usage, policy decisions, and idempotency keys.
The order map is intentionally a local fixture. A production tool must read the source system, enforce object ownership and tenant isolation there, and handle concurrent updates. To add refunds, do not expose a single issue_refund action. Split the flow into draft_refund, an approval record tied to exact parameters, and an idempotent submit_refund tool.
Choosing a Framework in 2026
Choose a runtime after defining the process, risk, and operational requirements. Framework popularity is not an architecture requirement.
| Option | Use it when | Trade-off |
|---|---|---|
| Provider API directly | The loop is short and you want complete control | You own state, dispatch, tracing, and recovery |
| OpenAI Agents SDK | You want lightweight Python/TypeScript primitives, tools, handoffs, sessions, and tracing | Closely aligned with OpenAI runtime capabilities |
| Claude Agent SDK | The agent needs a workspace, files, shell, and computer-like execution | Powerful environment requires strict sandboxing |
LangChain create_agent |
You need provider integrations and a standard tool-calling loop | Abstractions require disciplined version management |
| LangGraph | You need durable, stateful execution, checkpoints, and human interrupts | Lower-level graph design adds implementation work |
| CrewAI or AutoGen | Roles and multi-agent collaboration are central to the product | Coordination can add cost and obscure accountability |
LangChain v1 uses create_agent as its standard agent API; older tutorials based on create_react_agent should be treated as migration material. LangGraph focuses on orchestration capabilities such as persistence, durable execution, streaming, and human-in-the-loop rather than hiding prompts or architecture.
Single Agent or Multi-Agent?
Start with one agent and a small tool set. Add another agent only when at least one of these is true:
- a specialist needs a different permission boundary;
- context for one domain harms another domain's performance;
- independent work can run in parallel;
- separate teams own and evaluate separate capabilities.
Two common patterns are:
- Manager with agents as tools: one agent owns the user interaction and calls specialists for bounded tasks.
- Handoff: a triage agent transfers ownership and relevant context to a specialist.
Avoid multi-agent role-play that merely renames sequential prompts as "researcher," "analyst," and "writer." It increases model calls without necessarily improving evidence, control, or output quality.
Production Checklist
Before exposing an agent to users, verify:
- [ ] A deterministic workflow was considered first.
- [ ] Success, escalation, and stop conditions are explicit.
- [ ] Every tool has a narrow schema and documented side effects.
- [ ] Authorization is enforced in code, not only in prompts.
- [ ] External content and tool output are treated as untrusted.
- [ ] Consequential actions require specific human approval.
- [ ] Retries are bounded and write operations are idempotent.
- [ ] Working state can recover after process failure.
- [ ] Traces connect model decisions, tool calls, and business outcomes.
- [ ] Sensitive data is redacted and retention is defined.
- [ ] A representative evaluation set gates releases.
- [ ] Rollback or kill-switch behavior has been tested.
Frequently Asked Questions
What is the difference between an AI agent and RAG?
RAG retrieves context for a model. An agent controls a loop and can choose actions. An agent may use RAG as one tool, but retrieval alone does not make a system an agent.
Does an agent need long-term memory?
No. Many useful agents need only current-run state and fresh access to source systems. Add long-term memory when a concrete user benefit justifies its privacy, deletion, provenance, and staleness risks.
How do I prevent an agent from looping?
Set turn, tool-call, time, and cost limits. Detect repeated calls with equivalent arguments, require measurable progress after each observation, and escalate when the runtime cannot verify progress.
Should tool errors be returned to the model?
Return a bounded, actionable error category such as not_found, permission_denied, or temporary_failure. Do not expose stack traces, secrets, or raw infrastructure responses. Let the runtime decide which errors are retryable.
What should be versioned when releasing an agent?
Version the model and provider settings, instructions, context policy, tool schemas and implementations, runtime policy, identity scopes, state migration, evaluators, and rollback procedure in one deployment manifest. Test the composition, not only each component in isolation.
When should I use MCP?
Use MCP when tools or contextual resources need a standard interface across compatible hosts. MCP improves interoperability, but the host still owns user consent, server trust, authorization, and safe presentation of tool actions.
Conclusion
The essential unit of agent engineering is not the prompt; it is the controlled feedback loop. A production agent needs bounded tools, authoritative state, explicit stop conditions, human control over consequential actions, and evaluations tied to real outcomes.
Build the smallest loop that can solve the task. Make every action observable and recoverable. Only then increase autonomy.
Continue Reading
- Agent Loop Engineering goes deeper into progress detection, budgets, retries, and stop conditions.
- Anatomy of an Agent Harness separates model judgment from the runtime that owns context, tools, state, and control.
- Multi-Agent Orchestration Patterns explains when delegation is worth its coordination cost.
- The AI Agent and Agent Harness glossary entries provide concise definitions.