Agentic AI represents a fundamental shift from AI as a tool that responds to prompts to AI as an autonomous system that plans, executes, and adapts. Instead of asking an AI to answer a question, you give it a goal and it figures out the steps needed to achieve it.

This guide covers the architecture patterns behind production agentic AI systems, based on our experience building these systems at Cloudpush.

What Makes AI "Agentic"?

An AI agent has four key capabilities that distinguish it from a standard chatbot:

  1. Planning: Decomposing complex goals into actionable steps
  2. Tool Use: Interacting with external systems (APIs, databases, code execution)
  3. Memory: Maintaining context across multiple steps and sessions
  4. Reflection: Evaluating its own outputs and adjusting its approach

A simple chatbot responds to one message at a time. An agent takes a goal like "analyze our Q3 sales data and prepare a board presentation" and autonomously plans the steps, queries the database, analyzes the results, creates visualizations, and drafts the presentation.

Core Architecture Patterns

Pattern 1: ReAct (Reasoning + Acting)

The simplest and most widely used agent pattern. The agent alternates between reasoning about what to do and taking action:

Thought: I need to find the Q3 revenue data. Let me query the database.
Action: sql_query("SELECT * FROM revenue WHERE quarter = 'Q3' AND year = 2025")
Observation: [Results returned: 1,247 rows with revenue data by product line]
Thought: I have the raw data. Now I need to aggregate by product line and calculate growth.
Action: python_execute("df.groupby('product_line').agg({'revenue': 'sum'}).pct_change()")
Observation: [Growth rates: Enterprise +23%, SMB +12%, Consumer -5%]
Thought: Enterprise is growing strongly, Consumer is declining. Let me create a visualization.
Action: create_chart(data, type="bar", title="Q3 Revenue Growth by Product Line")

Implementation:

class ReActAgent:
    def __init__(self, llm, tools, max_steps=10):
        self.llm = llm
        self.tools = tools
        self.max_steps = max_steps

    async def run(self, goal: str) -> AgentResult:
        history = []
        for step in range(self.max_steps):
            # Reasoning step
            action = await self.llm.generate(
                system_prompt=self.system_prompt,
                messages=history,
                tools=self.tools.schemas()
            )

            if action.is_final_answer:
                return AgentResult(answer=action.content, steps=history)

            # Action step
            tool_result = await self.tools.execute(action.tool_call)
            history.append({"action": action, "observation": tool_result})

        return AgentResult(answer="Max steps reached", steps=history)

When to use ReAct: Simple, linear tasks where each step informs the next. Good for: data analysis, research queries, customer support.

Pattern 2: Plan-and-Execute

The agent creates a complete plan before executing, then follows the plan step by step:

class PlanAndExecuteAgent:
    def __init__(self, planner_llm, executor_llm, tools):
        self.planner = planner_llm
        self.executor = executor_llm
        self.tools = tools

    async def run(self, goal: str) -> AgentResult:
        # Planning phase
        plan = await self.planner.generate_plan(goal)

        results = []
        for step in plan.steps:
            # Execution phase
            result = await self.executor.execute_step(
                step=step,
                tools=self.tools,
                previous_results=results
            )
            results.append(result)

            # Optional: re-plan if results are unexpected
            if result.needs_replanning:
                remaining_steps = await self.planner.replan(
                    goal, plan, results
                )
                plan.steps = plan.steps[:step.index+1] + remaining_steps

        return self.synthesize(results)

When to use Plan-and-Execute: Complex, multi-step tasks where upfront planning improves efficiency. Good for: report generation, workflow automation, project management.

Pattern 3: Hierarchical Agents

A supervisor agent manages specialized worker agents:

class SupervisorAgent:
    def __init__(self):
        self.workers = {
            "researcher": ResearchAgent(tools=[web_search, doc_retrieval]),
            "analyst": AnalysisAgent(tools=[sql_query, python_exec]),
            "writer": WritingAgent(tools=[template_engine, chart_creator]),
            "reviewer": ReviewAgent(tools=[fact_checker, grammar_checker])
        }

    async def run(self, goal: str):
        # Supervisor decides which worker to invoke
        while not self.is_complete():
            next_action = await self.decide_next_worker(goal, self.state)
            worker = self.workers[next_action.worker_name]
            result = await worker.execute(next_action.instruction)
            self.state.update(result)

        return self.state.final_output

When to use Hierarchical: Complex tasks requiring diverse skills. Good for: content creation pipelines, customer onboarding, incident response.

Tool Design for Agents

Tools are the bridge between the agent's reasoning and the real world. Well-designed tools make agents reliable; poorly designed tools make them unpredictable.

Tool Design Principles

class Tool:
    name: str           # Clear, descriptive name
    description: str    # When and why to use this tool
    parameters: Schema  # Strongly typed parameters
    returns: Schema     # Strongly typed return value

    async def execute(self, params) -> ToolResult:
        # Validate inputs
        validated = self.parameters.validate(params)

        # Execute with timeout
        result = await asyncio.wait_for(
            self._execute(validated),
            timeout=30.0
        )

        # Validate outputs
        return self.returns.validate(result)

Critical Tool Design Rules

  1. Descriptive names: search_customer_database not search_db
  2. Clear descriptions: Tell the agent WHEN to use the tool, not just what it does
  3. Typed parameters: Use JSON Schema or Pydantic models
  4. Error messages: Return actionable error messages the agent can reason about
  5. Idempotent where possible: Retrying a tool should be safe
  6. Rate limited: Prevent the agent from overwhelming external services

Memory Architecture

Working Memory

The agent's current task context. Managed through the conversation history with strategic summarization:

class WorkingMemory:
    def __init__(self, max_tokens=8000):
        self.messages = []
        self.max_tokens = max_tokens

    def add(self, message):
        self.messages.append(message)
        self._prune_if_needed()

    def _prune_if_needed(self):
        while self.token_count() > self.max_tokens:
            # Summarize oldest messages
            oldest = self.messages[:3]
            summary = self.summarize(oldest)
            self.messages = [summary] + self.messages[3:]

Episodic Memory

Past task executions that the agent can learn from:

class EpisodicMemory:
    async def store_episode(self, task, steps, outcome):
        await self.vector_store.upsert(
            text=f"Task: {task}\nOutcome: {outcome}",
            metadata={
                "steps": steps,
                "outcome": outcome.status,
                "timestamp": datetime.utcnow()
            }
        )

    async def recall_similar(self, current_task, top_k=3):
        return await self.vector_store.search(current_task, top_k=top_k)

Semantic Memory

Domain knowledge and facts the agent can reference:

class SemanticMemory:
    def __init__(self, knowledge_base):
        self.kb = knowledge_base  # Vector database with domain knowledge

    async def query(self, question):
        # RAG-style retrieval from the knowledge base
        relevant_docs = await self.kb.search(question, top_k=5)
        return relevant_docs

Production Safety for Agentic Systems

Agentic systems require additional safety measures beyond standard AI guardrails:

Execution Sandboxing

Never let agents run code or access systems without sandboxing:

class SandboxedCodeExecution:
    async def execute(self, code: str) -> ExecutionResult:
        container = await self.docker.create(
            image="python-sandbox:latest",
            mem_limit="512m",
            cpu_period=100000,
            cpu_quota=50000,       # 50% CPU
            network_disabled=True,  # No network access
            read_only=True,
            timeout=30
        )
        result = await container.exec(code)
        await container.remove()
        return result

Action Approval Gates

Critical actions require human approval:

class ApprovalGate:
    REQUIRES_APPROVAL = {
        "send_email": True,
        "modify_database": True,
        "create_ticket": True,
        "web_search": False,
        "read_document": False,
    }

    async def check(self, tool_name, params, context):
        if self.REQUIRES_APPROVAL.get(tool_name, True):
            approval = await self.request_approval(tool_name, params, context)
            if not approval.granted:
                return ToolResult(error="Action not approved by human reviewer")
        return None  # Proceed

Budget and Rate Limits

class AgentBudget:
    def __init__(self, max_cost=5.0, max_steps=50, max_time=300):
        self.max_cost = max_cost
        self.max_steps = max_steps
        self.max_time = max_time  # seconds
        self.current_cost = 0
        self.current_steps = 0
        self.start_time = time.time()

    def check(self, step_cost):
        self.current_cost += step_cost
        self.current_steps += 1
        elapsed = time.time() - self.start_time

        if self.current_cost > self.max_cost:
            raise BudgetExceeded(f"Cost limit: ${self.max_cost}")
        if self.current_steps > self.max_steps:
            raise StepLimitExceeded(f"Step limit: {self.max_steps}")
        if elapsed > self.max_time:
            raise TimeLimitExceeded(f"Time limit: {self.max_time}s")

Evaluation and Testing

Agent Evaluation Framework

class AgentEvaluator:
    async def evaluate(self, agent, test_cases):
        results = []
        for test in test_cases:
            result = await agent.run(test.goal)
            score = self.score(result, test.expected)
            results.append({
                "goal": test.goal,
                "success": score.passed,
                "steps_taken": len(result.steps),
                "cost": result.total_cost,
                "latency": result.total_time,
                "correctness": score.correctness,
                "efficiency": score.efficiency
            })
        return AgentEvalReport(results)

What to Test

  1. Task completion: Does the agent achieve the goal?
  2. Efficiency: How many steps/tokens/dollars does it take?
  3. Safety: Does the agent stay within its approved boundaries?
  4. Error recovery: How does the agent handle tool failures?
  5. Edge cases: What happens with ambiguous, impossible, or adversarial goals?

Getting Started

Agentic AI systems are the most exciting and challenging area of applied AI right now. The key is starting with well-defined, bounded tasks and gradually expanding the agent's autonomy as you build confidence in its reliability and safety.

Contact Cloudpush to discuss how agentic AI can automate complex workflows in your business.