Architecting Autonomous Agents: A Low-Level Implementation in Pure Python Without Frameworks
In the current AI landscape, the abstraction layers provided by frameworks like LangChain or CrewAI have made agentic development accessible. However, these abstractions often obscure the fundamental mechanics of how an LLM (Large Language Model) transitions from a simple text generator to an autonomous agent. To truly master agentic workflows, one must understand the underlying orchestration: the interaction between the model, the toolset, and the execution loop.
This post explores the engineering principles required to build a functional AI agent using nothing but pure Python and the OpenAI API. We will deconstruct the "Agent Trinity"—Model, Tools, and Loop—and implement a mini-version of an autonomous coding assistant.
The Agent Trinity: Model, Tools, and the Execution Loop
An AI agent is not merely a chatbot; it is a system capable of reasoning and acting upon its environment. This capability is derived from three core components:
- The Model (The Brain): At its essence, an LLM is a stateless text-in/text-out engine. Whether running locally via Ollama, LM Studio, or through a hosted API like OpenAI's GPT-4o series, the model performs the reasoning. It processes tokens and predicts the next sequence of tokens based on the provided context.
- Tools (The Hands): Tools are external functions that allow the model to interact with the real world. These can range from simple Python functions to complex Model Context Protocol (MCP) servers or web search APIs. The critical requirement for a tool is its Schema: a structured definition (usually JSON) that describes the function's name, purpose, and required parameters.
- The Loop (The Nervous System): This is the iterative process where the software observes the model's output, determines if a tool call is requested, executes the corresponding Python code, and feeds the result back into the model. Without this loop, you have a chatbot; with it, you have an agent.
Phase 1: Establishing the LLM Interface and Message Roles
The first step in implementation is establishing communication with the LLM via an API client. Using modern Python package managers like uv for dependency management, we can initialize a project and install the openai library to handle requests.
When interacting with models, understanding Message Roles is critical for controlling behavior:
- System Role: This sets the "persona" or instructions for the agent (e'g., "You are a helpful coding assistant"). It resides at the top of the context window and dictates the constraints of the model's reasoning.
- User Role: Represents the human input or the specific task prompt.
- Assistant Role: Represents the model's previous responses.
Because LLMs are inherently stateless, they possess no inherent memory of prior interactions. To simulate "memory," we must manually maintain an array of message objects and pass the entire conversation history back to the API with every new request. This collection of messages constitutes the Context Window.
Phase 2: Implementing Tool Schemas and Function Calling
The most complex part of agent engineering is bridging the gap between a text-based model and executable Python code. The model cannot "run" a function; it can only output a structured string indicating its intent to run a function.
To facilitate this, we define a Tool Schema. This schema must be passed within the tools parameter of the Chat Completion API. A robust schema includes:
- Type: Usually defined as
function. - Name and Description: Highly descriptive text that allows the model to decide when to invoke the tool.
- Parameters: An object defining the expected input types (e.g., a string for a file path) and required fields.
In our implementation, we developed four specific tools: list_files, read_file, write_file, and run_command. The run_command tool is particularly powerful as it utilizes Python's subprocess module to execute shell commands, allowing the agent to manipulate the local file system or install dependencies (like pygame).
Phase 3: Orchestrating the Execution Loop
The final piece of the architecture is the orchestration logic. The software must handle a specific sequence of events when a model requests a tool call:
- Detection: Parse the API response to check for the presence of
tool_calls. - Extraction: Extract the function name and the arguments (provided as a JSON string) from the assistant's message.
- Execution: Map the requested function name to a local Python function and execute it with the provided arguments.
- Feedback Loop: Capture the output of the tool (or any error, such as
FileNotFoundError) and append a new message to the history with the roletool. This contains the result of the execution. - Re-prompting: Send the updated message history back to the model. The model now "sees" the result of its action and can decide whether to continue or provide a final response to the user.
Handling Guardrails and Safety
When implementing tools like run_command or write_file, security is paramount. An autonomous agent with shell access can be destructive. A professional-grade implementation requires Guardrails—a mechanism where the software intercepts a tool call and prompts the human operator for approval before executing potentially high-risk operations.
Conclusion: The Power of Low-Level Implementation
By stripping away frameworks, we reveal that an AI agent is essentially a sophisticated loop of JSON parsing and function execution. While frameworks provide convenience, understanding this low-level orchestration allows developers to build highly specialized, lightweight, and efficient agents tailored to specific computational tasks—such as the automated creation and testing of Python games demonstrated in our implementation.