Latest Articles

AI Agent Architectures: From ReAct to Multi-Agent Systems

Introduction: AI agents represent the next evolution of LLM applications—systems that can reason, plan, and take actions to accomplish complex tasks autonomously. Unlike simple chatbots that respond to single queries, agents maintain state, use tools, and iterate toward goals. This guide covers the architectural patterns that make agents effective: the ReAct framework for reasoning and acting, planning strategies from simple to hierarchical, memory systems for maintaining context across interactions, tool use for extending capabilities, and multi-agent systems for complex workflows. Whether you’re building a research assistant, a coding agent, or an autonomous workflow system, these patterns will help you design agents that are capable, reliable, and controllable.

AI Agent Architectures
AI Agent: Planning, Reasoning, and Action Loop

ReAct Framework

from dataclasses import dataclass, field
from typing import Any, Optional, Callable
from abc import ABC, abstractmethod
from enum import Enum

class ActionType(Enum):
    """Types of agent actions."""
    
    TOOL_CALL = "tool_call"
    FINAL_ANSWER = "final_answer"
    THINK = "think"

@dataclass
class Thought:
    """Agent's reasoning step."""
    
    content: str
    step: int

@dataclass
class Action:
    """Agent action."""
    
    type: ActionType
    tool_name: str = None
    tool_input: dict = None
    answer: str = None

@dataclass
class Observation:
    """Result of an action."""
    
    content: str
    success: bool = True

@dataclass
class AgentStep:
    """A single step in agent execution."""
    
    thought: Thought
    action: Action
    observation: Observation = None

class ReActAgent:
    """ReAct (Reasoning + Acting) agent."""
    
    def __init__(
        self,
        llm_client: Any,
        tools: dict[str, Callable],
        max_steps: int = 10
    ):
        self.llm = llm_client
        self.tools = tools
        self.max_steps = max_steps
        self.history: list[AgentStep] = []
    
    async def run(self, task: str) -> str:
        """Execute task using ReAct loop."""
        
        self.history = []
        
        for step in range(self.max_steps):
            # Generate thought and action
            thought, action = await self._think_and_act(task, step)
            
            # Check for final answer
            if action.type == ActionType.FINAL_ANSWER:
                self.history.append(AgentStep(
                    thought=thought,
                    action=action
                ))
                return action.answer
            
            # Execute action
            observation = await self._execute_action(action)
            
            # Record step
            self.history.append(AgentStep(
                thought=thought,
                action=action,
                observation=observation
            ))
            
            # Check for errors
            if not observation.success:
                # Could retry or adjust strategy
                pass
        
        return "Max steps reached without finding answer."
    
    async def _think_and_act(
        self,
        task: str,
        step: int
    ) -> tuple[Thought, Action]:
        """Generate thought and decide action."""
        
        # Build prompt with history
        prompt = self._build_prompt(task)
        
        response = await self.llm.complete(prompt)
        
        # Parse response
        thought, action = self._parse_response(response.content, step)
        
        return thought, action
    
    def _build_prompt(self, task: str) -> str:
        """Build ReAct prompt."""
        
        tools_desc = "\n".join(
            f"- {name}: {func.__doc__ or 'No description'}"
            for name, func in self.tools.items()
        )
        
        history_text = ""
        for step in self.history:
            history_text += f"\nThought: {step.thought.content}"
            if step.action.type == ActionType.TOOL_CALL:
                history_text += f"\nAction: {step.action.tool_name}({step.action.tool_input})"
                history_text += f"\nObservation: {step.observation.content}"
        
        return f"""You are an AI assistant that solves tasks by thinking step-by-step and using tools.

Available tools:
{tools_desc}

Task: {task}

{history_text}

Think about what to do next, then either use a tool or provide the final answer.

Format your response as:
Thought: [your reasoning]
Action: [tool_name(input)] OR Final Answer: [your answer]"""
    
    def _parse_response(self, response: str, step: int) -> tuple[Thought, Action]:
        """Parse LLM response into thought and action."""
        
        lines = response.strip().split('\n')
        
        thought_content = ""
        action = None
        
        for line in lines:
            if line.startswith("Thought:"):
                thought_content = line[8:].strip()
            elif line.startswith("Action:"):
                action_text = line[7:].strip()
                action = self._parse_action(action_text)
            elif line.startswith("Final Answer:"):
                answer = line[13:].strip()
                action = Action(type=ActionType.FINAL_ANSWER, answer=answer)
        
        thought = Thought(content=thought_content, step=step)
        
        if action is None:
            action = Action(type=ActionType.THINK)
        
        return thought, action
    
    def _parse_action(self, action_text: str) -> Action:
        """Parse action text into Action object."""
        
        import re
        
        # Match tool_name(input)
        match = re.match(r'(\w+)\((.*)\)', action_text)
        
        if match:
            tool_name = match.group(1)
            tool_input = match.group(2)
            
            # Try to parse as JSON or dict
            try:
                import json
                input_dict = json.loads(tool_input)
            except json.JSONDecodeError:
                input_dict = {"input": tool_input}
            
            return Action(
                type=ActionType.TOOL_CALL,
                tool_name=tool_name,
                tool_input=input_dict
            )
        
        return Action(type=ActionType.THINK)
    
    async def _execute_action(self, action: Action) -> Observation:
        """Execute tool action."""
        
        if action.type != ActionType.TOOL_CALL:
            return Observation(content="No action to execute", success=True)
        
        if action.tool_name not in self.tools:
            return Observation(
                content=f"Unknown tool: {action.tool_name}",
                success=False
            )
        
        try:
            tool = self.tools[action.tool_name]
            result = tool(**action.tool_input)
            
            # Handle async tools
            if hasattr(result, '__await__'):
                result = await result
            
            return Observation(content=str(result), success=True)
        except Exception as e:
            return Observation(content=f"Error: {str(e)}", success=False)

Planning Strategies

from dataclasses import dataclass, field
from typing import Any, Optional
from abc import ABC, abstractmethod
from enum import Enum

class PlanStatus(Enum):
    """Status of a plan step."""
    
    PENDING = "pending"
    IN_PROGRESS = "in_progress"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class PlanStep:
    """A step in a plan."""
    
    id: str
    description: str
    status: PlanStatus = PlanStatus.PENDING
    result: str = None
    dependencies: list[str] = field(default_factory=list)

@dataclass
class Plan:
    """A plan for completing a task."""
    
    goal: str
    steps: list[PlanStep]
    current_step: int = 0

class Planner(ABC):
    """Abstract planner."""
    
    @abstractmethod
    async def create_plan(self, task: str) -> Plan:
        """Create a plan for the task."""
        pass
    
    @abstractmethod
    async def replan(self, plan: Plan, feedback: str) -> Plan:
        """Revise plan based on feedback."""
        pass

class SimplePlanner(Planner):
    """Simple sequential planner."""
    
    def __init__(self, llm_client: Any):
        self.llm = llm_client
    
    async def create_plan(self, task: str) -> Plan:
        """Create sequential plan."""
        
        prompt = f"""Create a step-by-step plan to accomplish this task.
Each step should be a single, actionable item.

Task: {task}

Plan (numbered list):"""
        
        response = await self.llm.complete(prompt)
        
        steps = []
        for i, line in enumerate(response.content.split('\n')):
            line = line.strip().lstrip('0123456789.-) ')
            if line:
                steps.append(PlanStep(
                    id=f"step_{i}",
                    description=line
                ))
        
        return Plan(goal=task, steps=steps)
    
    async def replan(self, plan: Plan, feedback: str) -> Plan:
        """Revise plan based on feedback."""
        
        completed = [s for s in plan.steps if s.status == PlanStatus.COMPLETED]
        remaining = [s for s in plan.steps if s.status == PlanStatus.PENDING]
        
        prompt = f"""Revise this plan based on the feedback.

Original goal: {plan.goal}

Completed steps:
{chr(10).join(s.description for s in completed)}

Remaining steps:
{chr(10).join(s.description for s in remaining)}

Feedback: {feedback}

Revised remaining steps (numbered list):"""
        
        response = await self.llm.complete(prompt)
        
        new_steps = list(completed)
        for i, line in enumerate(response.content.split('\n')):
            line = line.strip().lstrip('0123456789.-) ')
            if line:
                new_steps.append(PlanStep(
                    id=f"step_{len(completed) + i}",
                    description=line
                ))
        
        return Plan(goal=plan.goal, steps=new_steps, current_step=len(completed))

class HierarchicalPlanner(Planner):
    """Hierarchical task decomposition planner."""
    
    def __init__(self, llm_client: Any, max_depth: int = 3):
        self.llm = llm_client
        self.max_depth = max_depth
    
    async def create_plan(self, task: str) -> Plan:
        """Create hierarchical plan."""
        
        # First level decomposition
        high_level = await self._decompose(task, depth=0)
        
        # Recursively decompose complex steps
        all_steps = []
        for step in high_level:
            if await self._is_complex(step.description):
                substeps = await self._decompose(step.description, depth=1)
                all_steps.extend(substeps)
            else:
                all_steps.append(step)
        
        return Plan(goal=task, steps=all_steps)
    
    async def _decompose(self, task: str, depth: int) -> list[PlanStep]:
        """Decompose task into subtasks."""
        
        if depth >= self.max_depth:
            return [PlanStep(id=f"step_0", description=task)]
        
        prompt = f"""Break down this task into 3-5 subtasks.
Each subtask should be specific and actionable.

Task: {task}

Subtasks (numbered list):"""
        
        response = await self.llm.complete(prompt)
        
        steps = []
        for i, line in enumerate(response.content.split('\n')):
            line = line.strip().lstrip('0123456789.-) ')
            if line:
                steps.append(PlanStep(
                    id=f"step_{depth}_{i}",
                    description=line
                ))
        
        return steps
    
    async def _is_complex(self, task: str) -> bool:
        """Check if task needs further decomposition."""
        
        prompt = f"""Is this task complex enough to need breaking down into subtasks?
Answer YES or NO.

Task: {task}

Answer:"""
        
        response = await self.llm.complete(prompt)
        return "yes" in response.content.lower()
    
    async def replan(self, plan: Plan, feedback: str) -> Plan:
        """Revise hierarchical plan."""
        
        # Similar to simple planner but maintains hierarchy
        return await SimplePlanner(self.llm).replan(plan, feedback)

class DAGPlanner(Planner):
    """Plan with dependencies (DAG)."""
    
    def __init__(self, llm_client: Any):
        self.llm = llm_client
    
    async def create_plan(self, task: str) -> Plan:
        """Create plan with dependencies."""
        
        prompt = f"""Create a plan with dependencies for this task.
Some steps may depend on others being completed first.

Task: {task}

For each step, specify:
- Step description
- Dependencies (which steps must be done first)

Format:
1. [description] | depends on: none
2. [description] | depends on: 1
3. [description] | depends on: 1, 2

Plan:"""
        
        response = await self.llm.complete(prompt)
        
        steps = []
        for line in response.content.split('\n'):
            if '|' in line:
                parts = line.split('|')
                desc = parts[0].strip().lstrip('0123456789.-) ')
                
                deps = []
                if len(parts) > 1 and 'depends on:' in parts[1]:
                    dep_text = parts[1].split('depends on:')[1].strip()
                    if dep_text.lower() != 'none':
                        deps = [f"step_{int(d.strip()) - 1}" for d in dep_text.split(',') if d.strip().isdigit()]
                
                steps.append(PlanStep(
                    id=f"step_{len(steps)}",
                    description=desc,
                    dependencies=deps
                ))
        
        return Plan(goal=task, steps=steps)
    
    def get_executable_steps(self, plan: Plan) -> list[PlanStep]:
        """Get steps that can be executed (dependencies met)."""
        
        completed_ids = {s.id for s in plan.steps if s.status == PlanStatus.COMPLETED}
        
        executable = []
        for step in plan.steps:
            if step.status == PlanStatus.PENDING:
                if all(dep in completed_ids for dep in step.dependencies):
                    executable.append(step)
        
        return executable
    
    async def replan(self, plan: Plan, feedback: str) -> Plan:
        """Revise DAG plan."""
        return await self.create_plan(f"{plan.goal}\n\nPrevious attempt feedback: {feedback}")

Agent Memory Systems

from dataclasses import dataclass, field
from typing import Any, Optional
from abc import ABC, abstractmethod
from datetime import datetime
import json

@dataclass
class MemoryEntry:
    """A memory entry."""
    
    content: str
    timestamp: datetime = field(default_factory=datetime.now)
    importance: float = 0.5
    metadata: dict = field(default_factory=dict)

class AgentMemory(ABC):
    """Abstract agent memory."""
    
    @abstractmethod
    def add(self, content: str, **kwargs) -> None:
        """Add to memory."""
        pass
    
    @abstractmethod
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Retrieve relevant memories."""
        pass
    
    @abstractmethod
    def clear(self) -> None:
        """Clear memory."""
        pass

class WorkingMemory(AgentMemory):
    """Short-term working memory."""
    
    def __init__(self, max_entries: int = 10):
        self.max_entries = max_entries
        self.entries: list[MemoryEntry] = []
    
    def add(self, content: str, **kwargs) -> None:
        """Add to working memory."""
        
        entry = MemoryEntry(
            content=content,
            importance=kwargs.get('importance', 0.5),
            metadata=kwargs.get('metadata', {})
        )
        
        self.entries.append(entry)
        
        # Trim if over capacity
        if len(self.entries) > self.max_entries:
            # Remove least important
            self.entries.sort(key=lambda e: e.importance, reverse=True)
            self.entries = self.entries[:self.max_entries]
    
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Get recent memories."""
        
        # Return most recent
        return sorted(self.entries, key=lambda e: e.timestamp, reverse=True)[:k]
    
    def clear(self) -> None:
        self.entries = []

class EpisodicMemory(AgentMemory):
    """Memory of past episodes/experiences."""
    
    def __init__(self, embedding_model: Any, vector_store: Any):
        self.embedder = embedding_model
        self.store = vector_store
        self.entries: list[MemoryEntry] = []
    
    def add(self, content: str, **kwargs) -> None:
        """Add episode to memory."""
        
        entry = MemoryEntry(
            content=content,
            importance=kwargs.get('importance', 0.5),
            metadata=kwargs.get('metadata', {})
        )
        
        # Store with embedding
        embedding = self.embedder.embed(content)
        self.store.add(
            id=str(len(self.entries)),
            embedding=embedding,
            metadata={
                "content": content,
                "timestamp": entry.timestamp.isoformat(),
                "importance": entry.importance
            }
        )
        
        self.entries.append(entry)
    
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Retrieve relevant episodes."""
        
        query_embedding = self.embedder.embed(query)
        results = self.store.search(query_embedding, k=k)
        
        entries = []
        for r in results:
            entries.append(MemoryEntry(
                content=r.metadata["content"],
                timestamp=datetime.fromisoformat(r.metadata["timestamp"]),
                importance=r.metadata["importance"]
            ))
        
        return entries
    
    def clear(self) -> None:
        self.entries = []
        self.store.clear()

class SemanticMemory(AgentMemory):
    """Long-term knowledge memory."""
    
    def __init__(self, embedding_model: Any):
        self.embedder = embedding_model
        self.knowledge: dict[str, MemoryEntry] = {}
        self.embeddings: dict[str, Any] = {}
    
    def add(self, content: str, key: str = None, **kwargs) -> None:
        """Add knowledge to memory."""
        
        if key is None:
            key = content[:50]
        
        entry = MemoryEntry(
            content=content,
            importance=kwargs.get('importance', 0.5),
            metadata=kwargs.get('metadata', {})
        )
        
        self.knowledge[key] = entry
        self.embeddings[key] = self.embedder.embed(content)
    
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Retrieve relevant knowledge."""
        
        import numpy as np
        
        query_embedding = self.embedder.embed(query)
        
        # Calculate similarities
        similarities = []
        for key, emb in self.embeddings.items():
            sim = np.dot(query_embedding, emb)
            similarities.append((key, sim))
        
        # Sort by similarity
        similarities.sort(key=lambda x: x[1], reverse=True)
        
        return [self.knowledge[key] for key, _ in similarities[:k]]
    
    def clear(self) -> None:
        self.knowledge = {}
        self.embeddings = {}

class ReflectionMemory(AgentMemory):
    """Memory with reflection and synthesis."""
    
    def __init__(self, llm_client: Any, base_memory: AgentMemory):
        self.llm = llm_client
        self.base = base_memory
        self.reflections: list[MemoryEntry] = []
    
    def add(self, content: str, **kwargs) -> None:
        """Add and potentially reflect."""
        
        self.base.add(content, **kwargs)
        
        # Periodically reflect
        if len(self.base.entries) % 10 == 0:
            asyncio.create_task(self._reflect())
    
    async def _reflect(self) -> None:
        """Generate reflection on recent memories."""
        
        recent = self.base.retrieve("", k=10)
        
        prompt = f"""Reflect on these recent experiences and extract key insights.

Experiences:
{chr(10).join(e.content for e in recent)}

Key insights (2-3 sentences):"""
        
        response = await self.llm.complete(prompt)
        
        self.reflections.append(MemoryEntry(
            content=response.content,
            importance=0.8,
            metadata={"type": "reflection"}
        ))
    
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Retrieve memories and relevant reflections."""
        
        base_results = self.base.retrieve(query, k=k-1)
        
        # Add most relevant reflection
        if self.reflections:
            base_results.append(self.reflections[-1])
        
        return base_results
    
    def clear(self) -> None:
        self.base.clear()
        self.reflections = []

class CompositeMemory(AgentMemory):
    """Combine multiple memory systems."""
    
    def __init__(
        self,
        working: WorkingMemory,
        episodic: EpisodicMemory,
        semantic: SemanticMemory
    ):
        self.working = working
        self.episodic = episodic
        self.semantic = semantic
    
    def add(self, content: str, memory_type: str = "working", **kwargs) -> None:
        """Add to appropriate memory."""
        
        if memory_type == "working":
            self.working.add(content, **kwargs)
        elif memory_type == "episodic":
            self.episodic.add(content, **kwargs)
        elif memory_type == "semantic":
            self.semantic.add(content, **kwargs)
        else:
            # Add to all
            self.working.add(content, **kwargs)
            self.episodic.add(content, **kwargs)
    
    def retrieve(self, query: str, k: int = 5) -> list[MemoryEntry]:
        """Retrieve from all memories."""
        
        results = []
        
        # Get from each memory type
        results.extend(self.working.retrieve(query, k=2))
        results.extend(self.episodic.retrieve(query, k=2))
        results.extend(self.semantic.retrieve(query, k=2))
        
        # Sort by importance and recency
        results.sort(key=lambda e: (e.importance, e.timestamp), reverse=True)
        
        return results[:k]
    
    def clear(self) -> None:
        self.working.clear()
        self.episodic.clear()
        self.semantic.clear()

Multi-Agent Systems

from dataclasses import dataclass, field
from typing import Any, Optional, Callable
from abc import ABC, abstractmethod
from enum import Enum
import asyncio

@dataclass
class Message:
    """Message between agents."""
    
    sender: str
    recipient: str
    content: str
    message_type: str = "text"
    metadata: dict = field(default_factory=dict)

class Agent(ABC):
    """Abstract agent in multi-agent system."""
    
    def __init__(self, name: str, llm_client: Any):
        self.name = name
        self.llm = llm_client
        self.inbox: asyncio.Queue = asyncio.Queue()
    
    @abstractmethod
    async def process(self, message: Message) -> Optional[Message]:
        """Process incoming message."""
        pass
    
    async def send(self, recipient: str, content: str, **kwargs) -> Message:
        """Create message to send."""
        
        return Message(
            sender=self.name,
            recipient=recipient,
            content=content,
            **kwargs
        )

class SpecialistAgent(Agent):
    """Agent with specific expertise."""
    
    def __init__(
        self,
        name: str,
        llm_client: Any,
        expertise: str,
        system_prompt: str = None
    ):
        super().__init__(name, llm_client)
        self.expertise = expertise
        self.system_prompt = system_prompt or f"You are an expert in {expertise}."
    
    async def process(self, message: Message) -> Optional[Message]:
        """Process with expertise."""
        
        prompt = f"""{self.system_prompt}

Message from {message.sender}:
{message.content}

Your response:"""
        
        response = await self.llm.complete(prompt)
        
        return await self.send(
            recipient=message.sender,
            content=response.content
        )

class OrchestratorAgent(Agent):
    """Agent that coordinates other agents."""
    
    def __init__(
        self,
        name: str,
        llm_client: Any,
        agents: dict[str, Agent]
    ):
        super().__init__(name, llm_client)
        self.agents = agents
    
    async def process(self, message: Message) -> Optional[Message]:
        """Orchestrate task across agents."""
        
        # Decide which agent(s) to involve
        agent_names = list(self.agents.keys())
        
        prompt = f"""You are coordinating a team of specialists: {', '.join(agent_names)}

Task: {message.content}

Which specialist(s) should handle this? Respond with agent names separated by commas:"""
        
        response = await self.llm.complete(prompt)
        
        selected = [a.strip() for a in response.content.split(',')]
        
        # Delegate to selected agents
        results = []
        for agent_name in selected:
            if agent_name in self.agents:
                agent = self.agents[agent_name]
                sub_message = Message(
                    sender=self.name,
                    recipient=agent_name,
                    content=message.content
                )
                result = await agent.process(sub_message)
                if result:
                    results.append(f"{agent_name}: {result.content}")
        
        # Synthesize results
        synthesis_prompt = f"""Synthesize these specialist responses into a final answer.

Task: {message.content}

Responses:
{chr(10).join(results)}

Final answer:"""
        
        final = await self.llm.complete(synthesis_prompt)
        
        return await self.send(
            recipient=message.sender,
            content=final.content
        )

class DebateSystem:
    """Multi-agent debate for better reasoning."""
    
    def __init__(
        self,
        llm_client: Any,
        num_agents: int = 3,
        num_rounds: int = 2
    ):
        self.llm = llm_client
        self.num_agents = num_agents
        self.num_rounds = num_rounds
    
    async def debate(self, question: str) -> str:
        """Run debate and synthesize answer."""
        
        # Initial responses
        responses = []
        for i in range(self.num_agents):
            prompt = f"""You are Agent {i+1}. Answer this question with your best reasoning.

Question: {question}

Your answer:"""
            
            response = await self.llm.complete(prompt)
            responses.append(response.content)
        
        # Debate rounds
        for round_num in range(self.num_rounds):
            new_responses = []
            
            for i in range(self.num_agents):
                other_responses = [r for j, r in enumerate(responses) if j != i]
                
                prompt = f"""You are Agent {i+1}. Consider other agents' responses and refine your answer.

Question: {question}

Your previous answer: {responses[i]}

Other agents' answers:
{chr(10).join(f"Agent {j+1}: {r}" for j, r in enumerate(other_responses))}

Your refined answer (consider their points but maintain your reasoning):"""
                
                response = await self.llm.complete(prompt)
                new_responses.append(response.content)
            
            responses = new_responses
        
        # Final synthesis
        synthesis_prompt = f"""Synthesize these debated responses into a final answer.

Question: {question}

Final responses after debate:
{chr(10).join(f"Agent {i+1}: {r}" for i, r in enumerate(responses))}

Best synthesized answer:"""
        
        final = await self.llm.complete(synthesis_prompt)
        return final.content

class CrewSystem:
    """Crew of agents working together."""
    
    def __init__(self):
        self.agents: dict[str, Agent] = {}
        self.message_bus: asyncio.Queue = asyncio.Queue()
    
    def add_agent(self, agent: Agent):
        """Add agent to crew."""
        self.agents[agent.name] = agent
    
    async def run_task(self, task: str, coordinator: str) -> str:
        """Run task with coordinator."""
        
        if coordinator not in self.agents:
            raise ValueError(f"Unknown coordinator: {coordinator}")
        
        # Send task to coordinator
        message = Message(
            sender="user",
            recipient=coordinator,
            content=task
        )
        
        result = await self.agents[coordinator].process(message)
        
        return result.content if result else "No result"
    
    async def run_pipeline(
        self,
        task: str,
        pipeline: list[str]
    ) -> str:
        """Run task through agent pipeline."""
        
        current_content = task
        
        for agent_name in pipeline:
            if agent_name not in self.agents:
                continue
            
            message = Message(
                sender="pipeline",
                recipient=agent_name,
                content=current_content
            )
            
            result = await self.agents[agent_name].process(message)
            
            if result:
                current_content = result.content
        
        return current_content

Production Agent Service

from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing import Optional, Any
import asyncio
import uuid

app = FastAPI()

class TaskRequest(BaseModel):
    task: str
    agent_type: str = "react"
    max_steps: int = 10

class TaskResponse(BaseModel):
    task_id: str
    status: str
    result: Optional[str] = None
    steps: list[dict] = []

# Task storage
tasks: dict[str, dict] = {}

# Mock agent for demo
class MockAgent:
    async def run(self, task: str) -> str:
        await asyncio.sleep(1)
        return f"Completed task: {task}"

agent = MockAgent()

async def run_agent_task(task_id: str, task: str, max_steps: int):
    """Run agent task in background."""
    
    tasks[task_id]["status"] = "running"
    
    try:
        result = await agent.run(task)
        tasks[task_id]["status"] = "completed"
        tasks[task_id]["result"] = result
    except Exception as e:
        tasks[task_id]["status"] = "failed"
        tasks[task_id]["error"] = str(e)

@app.post("/v1/tasks")
async def create_task(
    request: TaskRequest,
    background_tasks: BackgroundTasks
) -> TaskResponse:
    """Create agent task."""
    
    task_id = str(uuid.uuid4())
    
    tasks[task_id] = {
        "status": "pending",
        "task": request.task,
        "result": None,
        "steps": []
    }
    
    background_tasks.add_task(
        run_agent_task,
        task_id,
        request.task,
        request.max_steps
    )
    
    return TaskResponse(
        task_id=task_id,
        status="pending"
    )

@app.get("/v1/tasks/{task_id}")
async def get_task(task_id: str) -> TaskResponse:
    """Get task status."""
    
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    
    task = tasks[task_id]
    
    return TaskResponse(
        task_id=task_id,
        status=task["status"],
        result=task.get("result"),
        steps=task.get("steps", [])
    )

@app.delete("/v1/tasks/{task_id}")
async def cancel_task(task_id: str) -> dict:
    """Cancel task."""
    
    if task_id not in tasks:
        raise HTTPException(status_code=404, detail="Task not found")
    
    tasks[task_id]["status"] = "cancelled"
    
    return {"status": "cancelled"}

@app.get("/v1/tasks")
async def list_tasks() -> list[TaskResponse]:
    """List all tasks."""
    
    return [
        TaskResponse(
            task_id=tid,
            status=task["status"],
            result=task.get("result")
        )
        for tid, task in tasks.items()
    ]

@app.get("/health")
async def health():
    return {"status": "healthy"}

References

Conclusion

Agent architectures transform LLMs from reactive responders into proactive problem solvers. The ReAct framework provides the foundation—interleaving reasoning and action creates more reliable behavior than either alone. Planning is essential for complex tasks; start with simple sequential plans and add hierarchy or dependencies as needed. Memory systems determine what agents can remember and learn; working memory for immediate context, episodic memory for past experiences, and semantic memory for accumulated knowledge. Tool use extends agent capabilities beyond text generation; design tools with clear interfaces and good error messages. Multi-agent systems enable specialization and collaboration; orchestrators coordinate specialists, debates improve reasoning through diverse perspectives, and pipelines enable sequential processing. In production, implement proper task management with status tracking, cancellation, and timeout handling. Monitor agent behavior closely—autonomous systems can fail in unexpected ways. Start with constrained agents that have limited tools and clear boundaries, then expand capabilities as you understand their behavior. The key insight is that agents are systems, not just prompts—architecture decisions about planning, memory, and coordination matter as much as the underlying LLM.


Discover more from Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

About the Author

I am a Cloud Architect and Developer passionate about solving complex problems with modern technology. My blog explores the intersection of Cloud Architecture, Artificial Intelligence, and Software Engineering. I share tutorials, deep dives, and insights into building scalable, intelligent systems.

Areas of Expertise

Cloud Architecture (Azure, AWS)
Artificial Intelligence & LLMs
DevOps & Kubernetes
Backend Dev (C#, .NET, Python, Node.js)
© 2025 Code, Cloud & Context | Built by Nithin Mohan TK | Powered by Passion