I isolated an architectural pattern that shows one role receive a task, performs a specific action, posts a message, and the next role continues from that message.
What This Agent Does
This agent receives a fictional business idea and sends it through a two-role workflow.
The first role is the Strategist. It turns the idea into a three-point action plan. The second role is the ExecutiveWriter. It reads the Strategist’s plan from a shared environment and writes a short executive summary.
The important part is the handoff. The roles do not call each other directly. They communicate through a shared message list, which makes the workflow visible, inspectable, and easier to reason about.
The Dispatch Desk
The clearest way to understand this build is to picture a dispatch desk.
A request lands on the desk. The Strategist picks it up, creates a plan, and puts the plan back on the desk. The ExecutiveWriter watches for that kind of plan, picks it up, and writes the final summary.
That shared desk is the environment. It keeps the workflow organized. It also makes the system easier to inspect because every step leaves behind a message.
Vocabulary You Need
Role: A role is a specific worker identity inside the system. In this build, the Strategist and ExecutiveWriter are roles.
Action: An action is one bounded task the role performs. The Strategist plans, and the ExecutiveWriter summarizes.
Message: A message is the packet of work passed between roles. It contains who sent it, who receives it, what kind of action it represents, and the content.
Environment: The environment is the shared place where messages are stored. It acts like the workflow desk.
Handoff: A handoff is the moment one role finishes work and passes the result to the next role.
What This Is Not
This is not the full MetaGPT framework.
It does not include production team orchestration, code execution, file generation, browser automation, retrieval systems, deployment tooling, or long-term memory.
I kept the build narrow on purpose. The goal was to expose the role, action, message, and environment pattern in a form that can be read quickly and tested locally.
Why I Built This
You will hear talks about autonomous teams, virtual companies, and agent swarms. Underneath that language, the core pattern is usually simpler: define roles, give each role one job, and make the handoff explicit.
What Actually Happened
What broke: The first draft had formatting and syntax problems. The first install also exposed a dependency conflict, and the first runtime test treated two model names as one model string.
What surprised me: The strongest issue was not the agent architecture. It was the operational detail around provider testing, dependency pins, and clean multi-model execution.
What I would change next: I would add a handoff contract validator. That would check whether each message has the right sender, recipient, action type, and required content before the next role acts.
Why this matters: Multi-agent systems fail between agents as much as inside agents. The handoff is where context can get vague, incomplete, or misrouted.
The Architecture: How It Works
The build has four moving parts.
The Message object carries work from one role to the environment. The Environment stores messages. The Action object wraps one model call. The Role watches for one kind of message and responds with one action.
The loop is intentionally small:
User idea
Strategist action
Strategy message
ExecutiveWriter action
Executive summary
That is enough to prove the architecture without recreating the original framework.
What to Watch When You Run It
The terminal prints the active model before each full run.
In my test, gemini-flash ran all three fictional scenarios first, then deepseek-v4-pro ran the same three scenarios. That made provider differences visible without changing the code.
The key thing to watch is the role line:
[Strategist] acting on: business_idea
Then:
[ExecutiveWriter] acting on: strategy_plan
Those lines prove the system is not just producing one large answer. It is moving work through a defined handoff.
The Build: Step by Step
@dataclass
class Message:
sender: str
recipient: str
action: str
content: str
class Environment:
def __init__(self):
self.messages = []
def publish(self, message):
self.messages.append(message)
def observe(self, action_filter):
matches = [message for message in self.messages if message.action == action_filter]
return matches[-1] if matches else None
What This Code Is Actually Doing
This is the shared desk. A Message is one piece of work, with a sender, recipient, action type, and content. The Environment stores those messages and lets a role look up the latest message for the action it cares about.
class Action:
def __init__(self, name, instruction, client, model):
self.name = name
self.instruction = instruction
self.client = client
self.model = model
def run(self, input_text):
return call_model(self.client, self.model, self.instruction, input_text)
class Role:
def __init__(self, name, watches, output_action, action):
self.name = name
self.watches = watches
self.output_action = output_action
self.action = action
def act(self, environment):
observed = environment.observe(self.watches)
if not observed:
return None
print(" [" + self.name + "] acting on: " + observed.action)
result = self.action.run(observed.content)
return Message(self.name, "Environment", self.output_action, result)
What This Code Is Actually Doing
An Action is the job a role performs. A Role watches the environment for a specific type of message. When it sees the message it needs, it runs its action and returns a new message for the next role.
def load_settings():
"""Load .env values and stop early if required settings are missing."""
load_dotenv()
required_settings = ["LITELLM_BASE_URL", "MODEL_NAME", "LITELLM_API_KEY"]
missing_settings = [name for name in required_settings if not os.getenv(name)]
if missing_settings:
print("Missing required environment variables: " + ", ".join(missing_settings))
print("Copy .env.template to .env and fill in the placeholder values.")
sys.exit(1)
return {
"base_url": os.getenv("LITELLM_BASE_URL"),
"models": parse_model_names(os.getenv("MODEL_NAME")),
"api_key": os.getenv("LITELLM_API_KEY"),
}
def parse_model_names(model_names):
"""Split a comma-separated MODEL_NAME value into clean model names."""
models = [model.strip() for model in model_names.split(",") if model.strip()]
if not models:
print("MODEL_NAME must contain at least one model name.")
sys.exit(1)
return models
What This Code Is Actually Doing
This section loads the model configuration from the local environment file. It also handles the multi-provider test pattern I use in this series. If MODEL_NAME contains two names separated by a comma, the script runs the same workflow once for each model.
def call_model(client, model, system_prompt, user_prompt):
"""Call the active model with three total attempts for rate limits."""
for attempt in range(1, 4):
try:
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
)
return response.choices[0].message.content.strip()
except RateLimitError:
if attempt == 3:
print("Rate limit reached after three attempts. Try again later.")
sys.exit(1)
print("Rate limit hit. Waiting before retry " + str(attempt + 1) + ".")
time.sleep(2**attempt)
except APIConnectionError:
print("Cannot reach the LiteLLM service. Check that it is running.")
sys.exit(1)
except Exception:
print("The model call failed. Check your LiteLLM configuration and try again.")
sys.exit(1)
What This Code Is Actually Doing
This is the model call boundary. The script sends a system prompt and user prompt through an OpenAI-compatible client pointed at LiteLLM. The retry loop gives rate limits three total attempts, and the error messages stay plain English instead of exposing raw provider details.
def run_company(organization_name, idea, environment, strategist, executive_writer):
"""Run one complete Strategist to ExecutiveWriter handoff."""
environment.messages.clear()
print("\nOrganization: " + organization_name)
print("Idea: " + idea)
environment.publish(Message("User", "Strategist", "business_idea", idea))
strategy_message = strategist.act(environment)
if not strategy_message:
print("Strategist produced no output.")
return
environment.publish(strategy_message)
print("\nStrategy Plan:\n" + strategy_message.content)
summary_message = executive_writer.act(environment)
if not summary_message:
print("ExecutiveWriter produced no output.")
return
environment.publish(summary_message)
print("\nExecutive Summary:\n" + summary_message.content)
What This Code Is Actually Doing
This is the assembly line. The user idea becomes a message. The Strategist reads that message and publishes a strategy plan. The ExecutiveWriter reads the strategy plan and publishes the executive summary. The terminal output makes each handoff visible.
def build_roles(client, model):
"""Create the two fixed roles used by the micro company workflow."""
strategist_instruction = (
"You are a business strategist. Given a business idea, produce exactly "
"three numbered action points. Each point must be one sentence. No additional text."
)
writer_instruction = (
"You are an executive writer. Read the strategy plan and write a concise "
"executive summary in 100 words or fewer. Use plain English. No bullet points. No headers."
)
strategist_action = Action("PlanStrategy", strategist_instruction, client, model)
writer_action = Action("WriteExecutiveSummary", writer_instruction, client, model)
return (
Role("Strategist", "business_idea", "strategy_plan", strategist_action),
Role("ExecutiveWriter", "strategy_plan", "executive_summary", writer_action),
)
What This Code Is Actually Doing
This section defines the two workers. The Strategist watches for a business_idea and produces a strategy_plan. The ExecutiveWriter watches for a strategy_plan and produces an executive_summary. That is the whole multi-agent pattern in a small, inspectable form.
Errors I Hit During This Build
The first dependency file pinned python-dotenv==1.2.1. LiteLLM 1.83.7 required python-dotenv==1.0.1, so pip returned a dependency resolution error. I changed the pin to python-dotenv==1.0.1, then the install completed successfully.
Why This Matters
The value of this build is not that it writes a strategy plan.
The value is that it shows the control surface of a multi-agent system. Once work moves through roles and messages, the workflow becomes easier to inspect, test, and govern.
That is where the next layer of enterprise value appears. The real opportunity is not just more agents. It is better handoffs, clearer contracts, and workflows that reveal exactly how a result was produced.
Tools Used
MetaGPT (MIT): https://github.com/FoundationAgents/MetaGPT
LiteLLM (MIT): https://github.com/BerriAI/litellm
OpenAI Python SDK (Apache 2.0): https://github.com/openai/openai-python
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.