Add a Topology#
A topology is a way of wiring agents together, implemented once per dataset as a thin runner on top of core/. This page covers adding a new topology, or a new framework implementation of an existing one, from the runners to the bridge adapters the optimizers use.
Choose what you are adding#
| Adding | Runner path | Seed prompts | Bridge key |
|---|---|---|---|
| A framework variant of an existing topology | topologies/<topology>/<fw>/<ds>/<fw>_<ds>.py |
reuse configs/prompts/<topology>/<ds>/ |
<topology>_<fw> |
| A new topology | topologies/<topo>/[<fw>/]<ds>/<fw>_<ds>.py |
new configs/prompts/<topo>/<ds>/ |
<topo> |
The existing variants follow the first row: the CrewAI, AutoGen and OpenAI Agents SDK runners read the prompts of sequential/, centralized/ and decentralized/, and the bridge calls them sequential_crewai, centralized_autogen and decentralized_openai_agents. Workflow Topologies describes the five shapes that exist today.
1. Write one runner per dataset#
Add nine runners, one per dataset folder. Each wires its framework and takes everything else from core: the task module core/tasks/<ds>.py (loader, prompt text, tools, scorer, records), core.prompts.role_prompt for the seed prompt plus its output contract, core.llm for the model clients, core.teams for the team, and core.cli.main for the shared command line. Add a Dataset shows the module shape, and the runners of an existing topology are the closest template.
Keep the module-level names the adapters patch: _load_prompt or SYSTEM_PROMPT, _build_llm or _build_client, VLLM_BASE_URL, MODEL_ID, N_AGENTS and N_ROUNDS where the team shape is configurable, _RECURSION_LIMIT, _OUTPUT_FORMAT_NUDGE, and solve, run_batch and main. Read them at call time so a patched value takes effect.
For token and call counts, pass the framework's output through the matching extractor in core/telemetry.py (langchain_telemetry, langchain_ensemble_telemetry, crewai_telemetry, autogen_telemetry, or openai_sdk_accumulate per call), then normalize.
Pattern demos sit next to the runners. No runner imports them:
| File | Pattern |
|---|---|
topologies/single/langgraph_base.py |
LangGraph create_react_agent |
topologies/independent/langgraph_base.py |
LangGraph Send fan-out and fan-in |
topologies/sequential/crewai/crewai_base/ |
CrewAI Process.sequential crew |
topologies/centralized/autogen/autogen_base.py |
AutoGen SelectorGroupChat |
topologies/decentralized/openai_agents/agents_sdk_base.py is different: it is the debate engine the Agents SDK runners share, and it loads the isolated SDK install.
2. Define the team and the prompts#
A framework variant reuses its topology's team spec and prompts, so skip to step 3. For a new topology:
- Team. Add a
<topo>:entry to eachconfigs/teams/<ds>.yamland a builder for it in_FIELDSincore/teams.py, which turns the YAML into aTeamSpecfor r ∈ {2, 4, 8, 10}. The team-spec README documents the existing schemas. - Roles. Add a block to
configs/prompts/roles.yamlwith adescriptionof the topology and, underbenchmarks, one entry per dataset listing<role>: "<job description>". Add<topo>:toconfigs/prompts/tools.yamlwith a tool list per dataset. - Prompts. Run
python configs/generate_role_prompts.py --only <topo>to writeconfigs/prompts/<topo>/<ds>/<role>.txt; Add a Dataset describes the generator. - Contract. Teach
output_contract()incore/output_contracts.pywhich roles produce the final answer. It recognizes onlysingle,independent,sequential,sequential_crewai,centralized,centralized_autogen,decentralizedanddecentralized_openai_agents; any other name gets no contract. Make the same change inoptimizers/bridge/output_contracts.py.
3. Add it to the launchers#
scripts/run_topologies.sh lists runner folders as path:framework pairs in TOPOS and skips any module that does not exist:
TOPOS="single:langgraph independent:langgraph \
sequential/langgraph:langgraph sequential/crewai:crewai \
centralized/langgraph:langgraph centralized/autogen:autogen \
decentralized/langgraph:langgraph decentralized/openai_agents:openai_agents"
Append your variant, for example sequential/<fw>:<fw>.
4. Bridge adapters#
The optimizers reach a runner through an adapter in optimizers/bridge/adapters/ that implements RealRunnerAdapter (adapter_protocol.py): roles(), get_prompt(role), set_prompt(role, text), reset(), run_example(example) and format_role_trace(role, output). run_example returns a dict with model_output (what the metric reads) and, optionally, winner and buckets. Keep state per instance; MIPRO's demos arrive through set_prompt(), so an adapter needs no optimizer-specific code.
For a framework variant, subclass the existing adapter of each dataset and change three attributes. This is how the CrewAI MATH adapter is defined:
class SequentialCrewAIMATHAdapter(SequentialMATHAdapter):
topology = "sequential_crewai"
framework = "crewai"
module_name = "topologies.sequential.crewai.math.crewai_math"
"sequential_crewai": "optimizers.bridge.adapters.module_math:SequentialCrewAIMATHAdapter",
For a new topology, also set prompt_topology (the prompt folder) and roles_ (the role files to load). Two places key on names:
- The
frameworkattribute picks the client the adapter injects into_build_llmand_build_client(patched_moduleinadapters/module_common.py). A new framework needs its own branch there. default_adapter_kwargsinoptimizers/protocol/runner.pypasses the team size asn_agentstoindependentanddecentralizedadapters andn_rounds=2todecentralizedones. Add your topology if its team shape is configurable.
Registry values are import strings, so a framework is imported only when its pair is requested. The bridge README has the full checklist and the adapter template.
5. Run it through the protocol#
optimizers/protocol/cells.py maps a cell to a bridge key: registry_key() and parse_registry_key() know the five topologies (TOPOLOGIES in config.py) and the three framework keys in FRAMEWORK_KEYS. Teach them your name; a framework also goes into the --framework choices in run.py. The experiment grid (build_grid) and its frozen sizes (EXPECTED_COUNTS) describe the published experiments, so a new pair runs with --allow-any-cell and is reported as non-conformant.
python -m optimizers.protocol.run --method gepa --dataset math --topology sequential_<fw> \
--model qwen --seed 0 --budget 56 --phase optimize --allow-any-cell \
--out runs/smoke/math_sequential_<fw>
Expect job.json, optimization.json and optimization/optimizer_result.json under --out. The golden inventory is derived from the file tree and the bridge registry, so new runners and pairs show up in test_inventory; record them with python -m tests.golden.record --only '<glob>'.
Checklist#
- Nine runners on
core, keeping the module-level names the adapters patch. - New topology only: team specs and their
core/teams.pybuilder, YAML entries and generated prompts, final roles in bothoutput_contracts.pycopies. TOPOSinscripts/run_topologies.sh.- Adapters and
DATASET_ADAPTERSentries, pluspatched_moduleanddefault_adapter_kwargschanges if needed. - The protocol's cell mapping, a smoke demo and a
--batch --limitslice per runner, a smoke job and golden cells.