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:

  1. The task requires several steps or tools.
  2. The correct sequence cannot be fully known in advance.
  3. 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:

text
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.

flowchart LR U[User goal] --> P[Policy and input checks] P --> C[Context builder] C --> M[Model decision] M -->|tool call| A[Approval and authorization] A --> T[Tool execution] T --> V[Validate observation] V --> S[Persist state and trace] S --> C M -->|final answer| O[Output validation] O --> U M -->|budget exhausted| H[Human escalation]

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.

go
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:

  1. Build context from the goal and current state.
  2. Ask the model for a tool call, a final answer, or an escalation.
  3. Execute the permitted action and capture the observation.
  4. Update state and test a stop condition.

Every loop needs hard limits:

go
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:

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:

bash
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

Primary Sources