Add a Dataset#
A run is one dataset solved by one topology runner with one team and one communication format. Each piece is written once and parameters choose the run, so a new dataset is a task module, a team spec, a set of thin runners, seed prompts, its evaluation IDs and splits, and a bridge loader for the optimizers.
The steps use <ds> for the dataset's short name. Use the same name everywhere: several components find the dataset by that string.
| Piece | Lives in | Provides |
|---|---|---|
| Task module | core/tasks/<ds>.py |
Loading, prompt text, tools, answer extraction, scoring, records. |
| Seed prompts | configs/prompts/<topology>/<ds>/<role>.txt |
Each agent's seed system prompt, generated from YAML. |
| Output contract | core/output_contracts.py |
The protected final-output instruction of the answering roles. |
| Team | configs/teams/<ds>.yaml, read by core/teams.py (TeamSpec) |
The agents at team size r ∈ {2, 4, 8, 10}. |
| Topology runners | topologies/<topology>/[<framework>/]<ds>/<framework>_<ds>.py |
The framework wiring: graph, crew, group chat or debate. |
| Team sizes | teamsizes/<topology>/<ds>/<ds>_r<r>.py, via core/variant.py |
The LangGraph runner with another r. |
| Communication formats | core/communication.py (CommPolicy), communications/<topology>/<ds>/<ds>_<format>.py |
How agents report to each other, and the parse metrics. |
| Evaluation IDs and splits | benchmarks/<ds>/<ds>_eval_ids.json, <ds>_splits.json |
The reported instances and the optimizers' fixed splits. |
| Bridge | optimizers/bridge/datasets/<ds>.py, adapters/, registry.py |
The optimizers' loader, metric and adapters. |
All three runner families run the same runner module:
python -m topologies.<topology>.[<framework>.]<ds>.<framework>_<ds> # team r=4, freeform reports
python -m teamsizes.<topology>.<ds>.<ds>_r8 # the runner with TEAM_SIZE=8
python -m communications.<topology>.<ds>.<ds>_structured_soft # with COMMUNICATION_FORMAT preset
1. Task module#
core/tasks/<ds>.py holds what every topology shares, with no framework imports at module level.
- Constants.
DATASET, the source constants andSOURCE(the "loading ..." line). - Loader.
load_instances(limit=None, offset=0, only=None, ...):onlyselects IDs, thenoffsetandlimitapply. Dataset options are explicit parameters, declared byadd_arguments(parser). - Prompt text. The user message (
format_prompt) and prompt nudges, as constants. - Tools. Plain functions plus a factory taking the docstring (
make_x(doc)). A tool's docstring is its description as the model sees it, so every wording is a constant, indentation included; runners add their framework's wrapper. - Scoring. Answer extraction and the reference scorer, as published.
- Records.
record(inst, pred, **fields),summarize(records), progress lines andrun_batch(instances, row, *, out_path, verbose, label)overcore.batch.run_batch. - Reuse the shared core modules instead of copying them.
- Tests go to
core/tasks/tests/test_<ds>.py; tests of shared modules go totests/core/.
2. Team spec#
configs/teams/<ds>.yaml has one entry per multi-agent topology, following the schema: a replicated role (independent, decentralized), the r stages (sequential), or a manager and its r - 1 workers (centralized). recursion_limit is the same at every r and is omitted when the agents make plain model calls. All text is sent to the model verbatim.
3. Topology runners#
Write one module per (topology, framework), eight in all. Each has this shape:
TOPOLOGY = "centralized"
TEAM_SIZE = globals().get("TEAM_SIZE") # preset by the team-size variants
TEAM = teams.spec(TOPOLOGY, task.DATASET, TEAM_SIZE)
VLLM_BASE_URL = settings.base_url() # repointed per rollout by the optimizers
MODEL_ID = settings.model_id()
def _load_prompt(role: str) -> str:
return prompts.role_prompt(TOPOLOGY, task.DATASET, role)
def _build_llm():
return chat_openai(model=MODEL_ID, base_url=VLLM_BASE_URL)
def solve(...) -> dict: ... # one instance
def run_batch(instances, out_path=None, verbose=True) -> dict: ...
def main(argv=None) -> int:
return cli.main(argv, description=..., load_instances=task.load_instances, run_batch=run_batch,
demo=_canned_demo, source=task.SOURCE, add_arguments=task.add_arguments)
topologies/single/math/langgraph_math.py is a complete single-agent example. The core/llm.py docstring lists the client builders for every runner style.
Module attributes are an interface
Read them at call time, keep their names, and keep when they are called (how many clients a row builds, when a prompt is loaded). The optimizer adapters patch or read _load_prompt, SYSTEM_PROMPT, _build_llm, _build_client, VLLM_BASE_URL, MODEL_ID, N_AGENTS, N_ROUNDS, _RECURSION_LIMIT and _OUTPUT_FORMAT_NUDGE. HiveMind patches DELEGATION_TOOLS, DELEGATION_NAMES, MANAGER_TOOLS, _manager_tool_node and _MANAGER_TERMINATE_NUDGE of the centralized LangGraph runners. The golden harness reads solve, run_one, run_batch, load_instances and the scorers.
The command line comes from core.cli.main, which adds the shared runner options and the dataset's add_arguments:
- A dataset may also pass
default_limit, anepilog, an info mode (cli.InfoMode("--summary", dataset_summary): a JSON report instead of a run) andconfigure, applied to the parsed options first. - The loader,
run_batch,configureand the info report receive the options they declare as parameters. - Diagnostics go through
logging.getLogger(__name__); stdout is for results only.
4. Seed prompts#
Seed prompts are written by an LLM. For every (topology, dataset, role) in roles.yaml, configs/generate_role_prompts.py fills meta_prompt.txt with the dataset line from domains.yaml, the topology and role descriptions from roles.yaml and the tool list from tools.yaml. It sends the result as one user message, strips any <think> block and writes the reply to configs/prompts/<topology>/<ds>/<role>.txt.
Add the dataset to the three YAML files in configs/prompts/:
domains.yaml: one line,<ds>: "...". The generator only visits datasets listed here.roles.yaml: undertopologies.<topology>.benchmarksfor each of the five topologies,<ds>:with<role>: "<job description>". Role names must match what the runners and the team spec load.tools.yaml: under each topology,<ds>:with the tool list. A missing entry becomesNone., which tells the prompt to use no tools.
Then generate; existing files are skipped unless you pass --force:
python configs/generate_role_prompts.py --only single/<ds> --only independent/<ds> \
--only sequential/<ds> --only centralized/<ds> --only decentralized/<ds>
The generator defaults to Qwen/Qwen3.5-122B-A10B-FP8 at http://localhost:8200/v1, temperature 0 and seed 42 (options). The CrewAI, AutoGen and Agents SDK runners read the prompts of their topology folder.
5. Output contract#
Add one sentence to DATASET_CONTRACTS in core/output_contracts.py that names the final artifact your scorer reads, for example "End with exactly one final line: Answer:
singleandindependent: roles inPRIMARY_FINAL_ROLES(solver,caller,coder,patcher,planner);decentralized:debater;sequential:SEQUENTIAL_FINAL_ROLES["<ds>"];centralized:CENTRALIZED_FINAL_ROLES_BY_DATASET["<ds>"].
The bridge adapters attach their own copy at execution time, so make the same entries in optimizers/bridge/output_contracts.py, whose contracts also carry the PROTECTED FINAL OUTPUT CONTRACT: header. Output contracts explains how they are applied.
6. Team sizes#
A team-size module runs the topology runner's source in its own namespace with a preset parameter (core.variant), so it owns a full set of hooks:
"""Centralized <DATASET> with team size r=8: a manager and 7 workers."""
from core import variant
variant.load(globals(), "topologies.centralized.langgraph.<ds>.langgraph_<ds>", TEAM_SIZE=8)
- The runner derives everything size-dependent from
TEAM: agents, delegate tools,max_turnsand the default output folder. - API-Bank and ToolHop are the exception: their team sizes vote over r replicas of one role (
teamsizes/apibank_common.py,teamsizes/toolhop_common.py, loaded withTOPOLOGYandTEAM_SIZE), and their CrewAI and AutoGen runners are the LangGraph runners under anotherSTYLE.
7. Communication formats#
A multi-agent runner selects its format once, at load time, and never rebinds it, so runners loaded with different formats never share one:
COMMUNICATION_FORMAT = globals().get("COMMUNICATION_FORMAT", "freeform") # preset by communications/
COMMUNICATION = CommPolicy(COMMUNICATION_FORMAT, task.DATASET, TOPOLOGY)
def _load_prompt(role: str) -> str:
return COMMUNICATION.system_prompt(prompts.role_prompt(TOPOLOGY, task.DATASET, role))
COMMUNICATION.system_promptappends the format's contract wherever a system prompt is completed:_load_prompt, a module-levelSYSTEM_PROMPT, API-Bank's four-argument_load_promptand ToolHop's_system_prompt.COMMUNICATION.handoff(role, text, ...)renders one agent's output before another receives it.- API-Bank and ToolHop runners default to
None, not"freeform": outside a communications run their agents read each other's outputs as plain text. - Each
communications/<topology>/<ds>/<ds>_<format>.pycallscommunication_formats.install(globals(), topology=..., dataset=..., fmt=...). The dataset'sPairsubclass incommunications/communication_formats.pyloads the LangGraph runner with the format preset (core.variant.module) and defines the pair'ssolve(handoffs recorded, reports scored),run_one,run_batchandmain. - The bridge adapters load the runner the same way:
import_isolated_real_module(module_name, COMMUNICATION_FORMAT=fmt).
8. Evaluation IDs and splits#
Ship two files in benchmarks/<ds>/, with IDs equal to the id values of load_instances:
<ds>_eval_ids.json:dataset,sample,n,sourceandids, the reported instances.<ds>_splits.json: fixedtrain,validationandtestID lists.testis the evaluation set in the same order;trainandvalidationare drawn from the remaining pool and never overlap it.
Evaluation Protocol describes both files.
9. Optimizer bridge#
- Add
optimizers/bridge/datasets/<ds>.pyfromtemplates/dataset_template.py:load_all()returnsdspy.Examples with anidand atask_instanceinput, andmetric(example, prediction, ...)returnsdspy.Prediction(score=..., feedback=...). The protocol imports the module by the dataset name. - Add adapters under
optimizers/bridge/adapters/, usually amodule_<ds>.pywhose classes subclassModuleAdapterBase(module_common.py) asmodule_lcb.pydoes, and register them inDATASET_ADAPTERSinoptimizers/bridge/registry.py. Add a Topology covers the adapter contract. - A new dataset is outside the experiment grid, so protocol jobs on it need
--allow-any-celland are non-conformant. Run the adapter'srun_example()on one example, then the optimization phase on a smoke budget, and check thatjob.json,optimization.jsonandoptimization/optimizer_result.jsonexist under--out:
python -m optimizers.protocol.run --method gepa --dataset <ds> --topology single --model qwen \
--seed 0 --budget 56 --phase optimize --allow-any-cell --out runs/smoke/<ds>_single
10. Golden tests#
Add the dataset to tests/golden/cells.py and record its cells once with python -m tests.golden.record --only '<glob>'. From then on, change behavior only on purpose:
GOLDEN_WORKERS=16 HF_DATASETS_OFFLINE=1 HF_HUB_OFFLINE=1 PYTHONDONTWRITEBYTECODE=1 \
TOOLHOP_ALLOW_DATASET_EXEC=1 python -m pytest -q tests/golden tests/core core/tasks/tests optimizers
python -m tests.golden.record --only '<cells whose change is intended>' --force # after reading the diff
ruff check <files> && ruff format --check <files>
11. Launchers#
The sweep scripts list their datasets and row limits in code: add <ds> to the DATASETS default and the LIMIT table of scripts/run_topologies.sh, and of run_teamsizes.sh and run_communications.sh if you add those variants. A dataset missing from LIMIT in run_topologies.sh runs 50 rows.
Checklist#
core/tasks/<ds>.pywith its tests, andconfigs/teams/<ds>.yaml.- Eight topology runners, plus team-size and communication modules if you add those variants.
- YAML entries and generated prompts under
configs/prompts/<topology>/<ds>/. - Contract and final roles in
core/output_contracts.pyandoptimizers/bridge/output_contracts.py. <ds>_eval_ids.jsonand<ds>_splits.jsoninbenchmarks/<ds>/.- The bridge loader, adapters and registry entry, and a smoke job.
- Golden cells, and the launchers'
DATASETSandLIMIT.