On Day 15, it’s time to let our multi-agent system grow up. So far, we’ve hardcoded four agents in the code. Planner, Hacker, Safecracker, and Mole, all hardwired with their respective roles, tools, and permissions. It works, no question. But it doesn’t scale well, isn’t flexible, and doesn’t exactly make A/B testing easier.
Imagine you want to add a fifth agent. Or test different system prompts for various agents. Or try different tool combinations. With hardcoded agents, that means code changes, new commits, and deployments every time. That’s annoying and error-prone.
It gets even worse when we want to test different team constellations. Four agents work well, but what about three? Or six? Which agent should be the saboteur? The mole is obvious, but what if the hacker sabotages? Every variation requires code changes again.
And then there’s the OAuth problem. Every agent needs its permissions, its scopes, its tools. If you want to change tool distribution, you have to dig deep into the code. That’s not agile development anymore, that’s waterfall.
The good news… there’s a solution 😀 Configuration over Code. Agents should be loaded from a configuration file, not defined in code. Team changes should require editing a YAML file, not a code commit. And that’s exactly what we’re building today.
The Idea: Agents as Configuration
The concept is simple but brings many advantages. Instead of defining agents in code, we write a YAML file with all agent definitions. Each agent gets its own configuration. For example, Planner and Hacker become:
The system reads this config at startup and automatically creates all agents with their respective properties. And if you want a new or additional agent, you simply add a new entry. And if you want to change tool permissions, well, you just change the corresponding tools entry. The same applies to different system prompts, etc., etc.
This brings a few advantages:
Flexibility: We can add or remove any number of agents without changing code.
A/B Testing: Different config files for different experiments. We can test new agents or prompts without touching the code.
Version Control: All agent changes are traceable in Git through config diffs. Admittedly, they would be too if I changed them in code, but it doesn’t make things clearer.
Environment Separation: We can dynamically switch between a development config with three agents and a “production” config with the full team.
Ultimately, this is the difference between a simple, rigid prototype and a flexible platform.
Implementation
The implementation consists of multiple layers. First, we need a ConfigLoader that parses YAML and converts it into clean Python objects. Then a DynamicAgent that’s created entirely from the config. And finally, a DynamicAgentSystem that orchestrates everything.
Let’s start with the data structures. We use Python’s dataclasses for clean, type-safe configuration. Dataclasses enable clear separation of data and logic, which facilitates dynamic agent creation from YAML configurations. There are even automatically generated methods like init, repr, and eq and other standard methods. I don’t have to implement these methods error-free myself anymore 😄
The ConfigLoader reads the YAML file and creates these structures.
class ConfigLoader: @staticmethod def load_config(config_path: str) -> SystemConfig: with open(config_path, 'r') as f: config_data = yaml.safe_load(f) agents = [] for agent_data in config_data['agents']: agent = AgentConfig( name=agent_data['name'], role=agent_data['role'], tools=agent_data.get('tools', []), oauth_scopes=agent_data.get('oauth_scopes', []), system_prompt=agent_data['system_prompt'], is_saboteur=agent_data.get('is_saboteur', False) ) agents.append(agent) return SystemConfig( agents=agents, oauth_service=config_data['oauth_service'], tool_services=config_data['tool_services'], llm=config_data['llm'], memory_service=config_data['memory_service'], session=config_data['session'] )
This looks more elaborate than it actually is and is just boilerplate code. One of the big advantages: We validate the config at load time. Faulty YAML files are detected immediately, not at runtime when an agent tries to respond for the first time.
Dynamic Agents
The interesting part comes with agent creation. A DynamicAgent is built entirely from its AgentConfig. And here a custom-programmed class comes into play.
The agent gets its config complemented by three clients: LLM, OAuth, and Tools. If the config defines OAuth scopes, the agent automatically fetches a token. No more manual auth management, everything happens transparently based on configuration.
The respond() method is part of the DynamicAgent class and uses the config for system prompts and tool information:
The agent is dumb in the best sense. It knows nothing about its role, its tools, or its permissions. It reads everything from the config. This makes it extremely flexible because the same agent class can represent any agent.
If you stumbled over the messages list after we already implemented shared message storage… The messages list is just temporary formatting for the LLM API call, not persistent storage. The agent itself has no conversation history of its own – the central history resides in the DynamicAgentSystem with information compression (last 5 messages).
System Orchestration
The DynamicAgentSystem finally brings everything together. It loads the config, initializes the clients, and creates all agents:
The conversation logic remains identical to before. Agents respond one after another, messages are logged, history is stored. The only difference: The agents now come from config instead of being hardcoded.
def run_conversation(self, num_turns: int = None): if num_turns is None: num_turns = self.config.session['max_turns'] turn_order = self.config.session['turn_order'] for turn in range(num_turns): for agent_name in turn_order: agent = self.agents[agent_name] context = self.conversation_history[-5:] response = agent.respond(context) message = { "turn": turn + 1, "agent": agent_name, "role": agent.config.role, "message": response } self.conversation_history.append(message)
What Makes It So Beneficial?
One of the true advantages lies in the possibilities that configuration opens up. Here are concrete scenarios that now become trivial.
Scenario 1: A/B Testing System Prompts
If you want to find out what results differently configured agents lead to, you can simply create two config files:
# config_aggressive.yaml- name: "planner" system_prompt: "Be aggressive and take calculated risks..."# config_conservative.yaml- name: "planner" system_prompt: "Be extremely cautious and risk-averse..."
Then just run both and compare results. No code change, just a config switch.
Scenario 2: Tool Permission Experiments
What happens when the hacker has all tools versus specialized roles?
Is the obvious insider (Mole) easier to detect than a technical expert (Hacker)?
All these experiments are now config changes, not code changes. This involves much less work and is also significantly less error-prone.
Benefits in Professional Environments: Environment Separation
In professional setups, you need different configs for different environments. This now works elegantly:
agents_config_dev.yaml # Small team, fast iterationagents_config_staging.yaml # Complete team, pre-prod testsagents_config_prod.yaml # Production setup with all features
No more code branches needed for different environments. One codebase with different configs. And all config changes are traceable in Git:
$ git diff agents_config.yaml- system_prompt: "Be aggressive and take risks..."+ system_prompt: "Be cautious and thorough..."
You immediately see what changed. Code reviews become easier because agent changes are separated from logic changes.
Integration
The Dynamic Agent System isn’t an isolated feature. Over the past days, we’ve implemented several features. Slightly modified, many things reappear:
Day 8-9 (OAuth): Scopes now come from the config. Each agent automatically gets its token based on oauth_scopes.
Day 13 (Agent Tools): Tools are loaded from the tools list. No more manual tool assignment.
Day 14 (OAuth Tools): Tool permissions are enforced through oauth_scopes. Security already in the config.
Day 12 (Memory Service): The memory service URL comes from the memory_service config. Different environments can use different services.
This is now the point where individual features become a coherent system. Not through more code, but through better architecture.
Service Architecture
An important difference from previous days is that the Dynamic Agent System requires multiple simultaneously running services. Before, we mostly had individual services, now we’re dealing with a microservice architecture.
Dynamic Agent System
├── OAuth Service (8001) → Token management for all agents
├── Calculator (8002) → Tool for Safecracker
├── File Reader (8003) → Tool for Hacker
├── Database Query (8004) → Tool for Mole
└── LLM Studio (1234) → Gemma Model
Through the separation, each service has a clear responsibility (Separation of Concerns). They can be scaled arbitrarily and independently from each other. Each service can be updated individually, and problems can be isolated to individual services.
And because there are now several steps to start all required services, I also created a script that automates this.
Helper Scripts
To simplify starting and stopping, there are now helper scripts:
# Start all services./day_15/start_services.sh# Run Agent Systempython day_15/dynamic_agent_system.py# Stop all services./day_15/stop_services.sh
The start_services.sh script first cleans up old processes on the ports. Then it starts the four services in the background, shows PIDs and status, and redirects logs to the /tmp folder.
The stop_services.sh script finds all processes on ports 8001-8004 and terminates them with kill -9.
Unlike previous days where we only had to start one service process, we now need at least 5 running processes simultaneously (OAuth, the three tools, and LLM Studio). This is more realistic for professional environments but also more complex to manage.
The service URLs and ports are all defined in agents_config.yaml:
Different environments (Dev/Staging/Prod) can use different service URLs without code changes.
Outlook
The Dynamic Agent System is the foundation for the coming days. Tomorrow we’ll tackle Service Integration, where we bring together all services (OAuth, Tools, Memory, SQLite). Then the config becomes even more important because we’ll centrally manage service URLs, credentials, and connection settings.
After that comes Session Management with cross-session state management. The session config in our YAML will then become the central control instrument.
And in the end, we’ll have a system where you can orchestrate complex multi-agent scenarios through pure config changes. Without code changes, without deployments, without risk.
Summary
Today we transformed our multi-agent system from hardcoded to configuration-driven. This sounds like a small step but is fundamental.
What did we gain?
Unlimited agents without code changes
A/B testing through config switching
Environment separation (Dev/Staging/Prod)
Git-based change tracking
From 4 to N agents without refactoring
What does it cost?
A bit more boilerplate during setup
YAML syntax instead of Python (but that’s a feature)
A few additional abstraction layers
Multiple services simultaneously: Instead of one service process, we now need 5 (OAuth, three tools, and LLM)
Service management: starting, stopping, monitoring multiple processes
The cost-benefit ratio is still clear. The flexibility and realistic microservice architecture are worth the additional complexity. The helper scripts (start_services.sh, stop_services.sh) make management manageable.
The most important insight: A good config system turns a rigid prototype into a flexible platform. Not through more features, but through better architecture.
Tomorrow we’ll build on this and integrate all services. But the foundation is now in place.
Usage
Quick Start
# 1. Start services (OAuth + 3 Tools)./day_15/start_services.sh# 2. Start LM Studio with Gemma (Port 1234)# Manually in LM Studio GUI# 3. Run Agent Systempython day_15/dynamic_agent_system.py# 4. Stop services./day_15/stop_services.sh