EvoAgentX Core Agent Rebuild

EvoAgentX is a large framework for building agents, workflows, tools, memory systems, approval flows, optimizers, and evaluation loops.

I did not try to recreate all of that. For this rebuild, I pulled out the part that matters most for learning agent architecture: one agent receives a task, decides whether it needs a tool, calls one approved tool, observes the result, and produces a final answer.


What This Agent Does

This agent is a small execution loop inspired by EvoAgentX. It receives a business research task, asks the model what to do next, and accepts only two possible decisions.

The model can either request one approved tool call or return a final structured report.

The demo uses three fictional companies:

Lumina Home Goods shows steady growth and low operational risk.

Vector Freight shows declining reliability, rising complaints, and high risk.

Cedar Desk Software shows strong renewals with an onboarding bottleneck.

That gives the agent three different outcomes to handle: low risk, high risk, and medium risk.


The Operating Desk

The easiest way to think about this build is an operating desk.

A task lands on the desk. The agent reads it and asks: do I already have enough information, or do I need to open the approved tool drawer?

There is only one drawer in this rebuild: company_data_lookup.

That constraint is the point. A real framework can give agents dozens of tools, but the architecture becomes clearer when the first version has one decision boundary:

Use the approved tool, or produce the final report.

For example, when the task asks about Vector Freight, the agent does not guess from its own memory. It asks for the company data. The script returns the fictional operating data. Then the agent writes the final assessment from that returned data.

That is the execution pattern: decide, act, observe, answer.


Vocabulary You Need

Agent loop: The repeated cycle where an agent reads the current state, decides the next action, and continues until it has a final answer.

Tool call: A structured request from the model asking Python to run an approved function. In this build, the only approved function is company_data_lookup.

Structured JSON: A predictable data format that Python can parse. Instead of asking the model for loose text, I ask for a small object with fields such as action, tool_name, and company_name.

Tool allowlist: A list of approved tools the agent is allowed to use. This rebuild accepts only one tool name and blocks anything else.

Provider agnostic: The code can switch between models through configuration. I tested the same script with Gemini and DeepSeek by changing the environment configuration, not the Python logic.


What This Is Not

This is not a replacement for EvoAgentX.

It does not include workflow graphs, prompt optimizers, long-term memory, RAG, database storage, web APIs, human approval screens, Docker execution, benchmark datasets, or automatic agent generation.

Those features matter in a full framework, but they hide the core lesson when the goal is architectural literacy.

This rebuild isolates one pattern: a single agent using one approved tool through a controlled execution loop.


Why I Built This

I built this because large agent frameworks can make simple ideas look intimidating for anyone.

EvoAgentX is doing serious work at the framework level, but the center of gravity is still understandable: an agent needs instructions, a model connection, a way to request tools, a tool boundary, and a loop that knows when to stop.

That is the standard I wanted to expose. Not a product clone. A clear operating pattern that can be inspected, tested, and discussed.


What Actually Happened

What surprised me: Gemini and DeepSeek both followed the same tool pattern cleanly once the model list bug was fixed. Their final reports had different writing styles, but the architecture stayed stable.

What I would change next: I would add a small credential broker that grants short-lived mock permissions before a tool call. That would make the tool boundary even more realistic without turning the rebuild into enterprise infrastructure.

Why this matters: The build proved that the central EvoAgentX idea can be taught without carrying the whole framework into the demo. The loop, the tool call, and the final report are enough to expose the architecture.


The Architecture: How It Works

The agent has five moving parts.

First, it loads environment variables. These point the OpenAI-compatible client at a LiteLLM proxy.

Second, it reads one or more model names. A comma-separated value lets the same script test more than one provider.

Third, it defines one mock company lookup tool. This keeps the demo deterministic and avoids live web access.

Fourth, it prompts the model to return strict JSON. The model can return either a tool request or a final answer.

Fifth, it runs a loop with a hard stop. The agent gets three turns, one allowed tool call, and no unapproved tools.

The important part is the boundary. The model can request action, but Python decides whether that action is allowed.


What to Watch When You Run It

The first line to watch is the active model list.

In my run, the script printed:

Active models: gemini-flash, deepseek-v4-pro

That confirmed the code was testing both providers.

For each company, the clean pattern looked like this:

Turn 1: the model requested company_data_lookup.

Turn 2: the model produced the final report.

Lumina Home Goods came back as low risk.

Vector Freight came back as high risk.

Cedar Desk Software came back as medium risk.

The exact wording varied by model. That is normal. The important part is that both providers followed the same action structure.


The Build: Step by Step

The first decision was to make configuration external. The code reads the LiteLLM endpoint, model name, and API key from the environment.

REQUIRED_ENV = ["LITELLM_BASE_URL", "MODEL_NAME", "LITELLM_API_KEY"]

def get_client() -> OpenAI:
    """Build an OpenAI-compatible client pointed at the LiteLLM proxy."""
    return OpenAI(base_url=os.getenv("LITELLM_BASE_URL"), api_key=os.getenv("LITELLM_API_KEY"))

def get_model_names() -> list[str]:
    """Return one or more model names from the comma-separated MODEL_NAME value."""
    return [name.strip() for name in os.getenv("MODEL_NAME", "").split(",") if name.strip()]

What This Code Is Actually Doing

This is the provider switchboard.

The agent does not know whether it is using Gemini, DeepSeek, Claude, OpenAI, or another LiteLLM-supported model. It only knows that a LiteLLM-compatible endpoint exists and that the model name comes from the environment.

The get_model_names function fixed the real bug from testing. When I used gemini-flash,deepseek-v4-pro, the code now splits that into two separate model runs instead of sending the whole comma-separated string as one model name.

The next piece is the fictional company data. I kept the data small because the point is the tool boundary, not a giant database.

MOCK_COMPANY_DATA = {
    "Lumina Home Goods": {
        "revenue_trend": "steady +8% year over year", "operational_risk": "low",
        "support_tickets": "stable at 120 per month", "customer_retention": "87%",
        "notes": "No major red flags. Expanding into outdoor furniture.",
    },
    "Vector Freight": {
        "revenue_trend": "flat, slight decline last two quarters", "operational_risk": "high",
        "support_tickets": "rising, up 34% in 90 days", "customer_retention": "61%",
        "notes": "Delivery reliability dropped to 71%. Complaints are escalating.",
    },
    "Cedar Desk Software": {
        "revenue_trend": "growing +15% ARR", "operational_risk": "medium",
        "support_tickets": "moderate, mostly onboarding-related", "customer_retention": "91% renewal rate",
        "notes": "Strong product-market fit. Onboarding takes 3x industry average.",
    },
}

What This Code Is Actually Doing

This is the demo database.

It gives the agent three different business situations. Lumina is stable. Vector is deteriorating. Cedar is growing but has an operational bottleneck.

That variety matters. If every example had the same risk profile, the terminal output would not prove much. With three different cases, the agent has to produce different final judgments from different evidence.

The actual tool is intentionally simple.

def company_data_lookup(company_name: str) -> dict:
    """Return fictional mock data for one approved demo company."""
    company_data = MOCK_COMPANY_DATA.get(company_name)
    return {"company": company_name, "data": company_data} if company_data else {
        "error": "Company not found in demo dataset.",
        "available_companies": list(MOCK_COMPANY_DATA.keys()),
    }

What This Code Is Actually Doing

This function is the agent’s only approved hand.

The model can ask for company data, but Python controls what gets returned. The model does not browse the web, read files, query a real database, or call an external API.

That makes the tool call inspectable. When the model asks for Vector Freight, the script returns only the fictional Vector Freight record.

The system prompt forces the model into two possible output shapes.

SYSTEM_PROMPT = """You are a business research agent. Decide whether to look up fictional company data or answer directly.
Respond with strict JSON only. If you need company data, respond with:
{"action": "tool_call", "tool_name": "company_data_lookup", "company_name": "EXACT company name", "reason": "why you need this data"}
If you have enough information to answer, respond with:
{"action": "final_answer", "answer": {"summary": "...", "risk_level": "low|medium|high", "recommended_next_step": "..."}}
Only call company_data_lookup once. After receiving tool results, always produce a final_answer."""

What This Code Is Actually Doing

This is the agent’s operating contract.

The model is not being asked for a normal paragraph. It is being asked for machine-readable JSON.

That gives Python something it can inspect. If the action is tool_call, Python checks the tool name. If the action is final_answer, Python prints the report.

This is the difference between a chatbot and an agent loop. The model is not just writing. It is making a structured request that the program can evaluate.

The model call has retry handling and friendly failure messages.

def ask_model(client: OpenAI, model_name: str, messages: list[dict[str, str]]) -> str:
    """Send the message history to the active model and return response text."""
    for attempt_number in range(1, MAX_MODEL_ATTEMPTS + 1):
        try:
            response = client.chat.completions.create(
                model=model_name, messages=messages, temperature=0.2
            )
            return response.choices[0].message.content.strip()
        except RateLimitError:
            if attempt_number == MAX_MODEL_ATTEMPTS:
                stop("Rate limit persists after three attempts. Try again later.")
            print("Rate limit reached. Waiting before retry " + str(attempt_number + 1) + " of 3.")
            time.sleep(2 ** attempt_number)
        except APIConnectionError:
            stop("Cannot reach the LiteLLM endpoint. Check that your proxy is running.")
        except Exception:
            stop("The model returned an unexpected error. Check your LiteLLM proxy logs.")

What This Code Is Actually Doing

This is the model connector.

It sends the current conversation state to the selected model and returns the model’s text response.

The retry logic exists for rate limits. A rate limit means the provider is temporarily rejecting requests because the usage is too high for that moment.

The generic error message is intentionally plain. It avoids dumping provider internals or local configuration into the terminal.

The loop is where the agent behavior becomes visible.

for turn_number in range(1, MAX_AGENT_TURNS + 1):
    print("\n--- Turn " + str(turn_number) + " ---")
    try:
        decision = json.loads(ask_model(client, model_name, messages))
    except json.JSONDecodeError:
        print("Agent returned invalid JSON. Stopping this task.")
        return

    action = decision.get("action")
    if action == "tool_call":
        if tool_already_used:
            print("Agent requested a second tool call. Stopping this task.")
            return

        if decision.get("tool_name", "") != "company_data_lookup":
            print("Agent requested a tool that is not allowed. Stopping this task.")
            return

What This Code Is Actually Doing

This is the control desk.

The model returns a decision. Python parses it, checks it, and decides what happens next.

The agent is allowed one tool call. If the model asks for a second tool call, the script stops that task. If the model asks for any tool other than company_data_lookup, the script stops that task.

That is the key architectural lesson: the model proposes, the program disposes.

The final answer path prints the structured report.

if action == "final_answer":
    answer = decision.get("answer", {})
    if not isinstance(answer, dict):
        print("Agent final answer was not in the expected format. Stopping this task.")
        return
    print("AGENT DECISION: final answer ready\n\nFINAL REPORT:")
    print("  Summary:               " + str(answer.get("summary", "N/A")))
    print("  Risk Level:            " + str(answer.get("risk_level", "N/A")))
    print("  Recommended Next Step: " + str(answer.get("recommended_next_step", "N/A")))
    return

What This Code Is Actually Doing

This is the report printer.

The model does not get to send any random final format. The code expects an answer object with a summary, risk level, and recommended next step.

That gives the terminal output a consistent shape across providers. Gemini and DeepSeek wrote different summaries, but both fit the same report structure.


Why This Matters

The value of this rebuild is not the mock company lookup.

The value is seeing where the control boundary lives.

An agent framework can look like magic from the outside. Underneath, the important questions are concrete:

What instructions did the model receive?

What actions can it request?

Who checks whether the action is allowed?

What happens after the tool returns data?

How does the loop know when to stop?

This rebuild answers those questions in one file.

Once the pattern is visible, larger frameworks become easier to evaluate. EvoAgentX is no longer just a large repository. It becomes a set of architectural choices around agents, tools, workflows, memory, optimization, and execution control.


Tools Used

EvoAgentX (MIT): https://github.com/EvoAgentX/EvoAgentX

LiteLLM (MIT): https://github.com/BerriAI/litellm

OpenAI Python SDK (Apache 2.0): https://github.com/openai/openai-python

python-dotenv (BSD 3-Clause): https://github.com/theskumar/python-dotenv

NosisTech Agent Engineering (MIT): https://github.com/nosistech/agent-engineering


(c) 2026 NosisTech LLC. Licensed under CC BY 4.0.
Use freely, just credit us.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.