3 · Declare the world
Define the simulated environment your agent is tested against — the (optional) multi-agent topology and the (optional) ledger schema — and know when a single-agent system needs neither.
Your agent is registered and active. Before you write checks, you decide what world those checks run in. The world is the simulated environment the platform stands up around your agent: the tools it can call (simulated instead of hitting real APIs) and, optionally, two extra declarations that make grading sharper —
- a topology (
sub_agents+topology) that names the sub-agents in a multi-agent system and how they hand off, and - a ledger schema (
ledger_schema) that pins down the entity types and flags the simulated world-state may contain.
Both are optional. A single agent with no sub-agents and no simulated data needs neither — skip straight to creating a suite. This page shows you when each one earns its keep and how to declare it on the agent — either through the SDK (create_code_agent) or by importing JSON in the dashboard.
"World" here is the simulated world for tools-and-data agents. Coding agents (that operate on a real repo checkout) don't use a ledger schema — the ledger declarations on this page are for simulated-tool agents only.
Do you need to declare anything?
Walk this once. Most first agents answer "no" to both.
Multiple agents that hand off?
If a supervisor delegates to workers (OpenAI Agents handoffs, a LangGraph node graph), declare the topology so the reviewer knows who's who. A single agent: skip it.
Simulated data the tools read and write?
If your tools operate on entities (orders, tickets, accounts) that the simulator invents and mutates, a ledger schema closes the world so grading stays consistent. Stateless tools: skip it.
If both answers are "no", your world is just your tools_schema (set on the agent back in register) and you're done here.
Declare the topology (multi-agent only)
The topology is the declared possibility graph — what your wiring can do, authored before any run. It is two sibling keys on the agent's config:
{
"sub_agents": [
{ "actor_id": "triage", "tools": ["lookup_order"], "talks_to": ["refunds"] },
{ "actor_id": "refunds", "tools": ["issue_refund"] }
],
"topology": { "entry": "triage" }
}sub_agents— one entry per sub-agent.actor_idis required;toolsandtalks_toare optional lists.topology.entry— the sub-agent(s) the run launches into.
This is a prior, never a contract the platform blocks runs against: a sub-agent that shows up at runtime but wasn't declared surfaces honestly as undeclared in the trace rather than failing the run.
The easy path: extract it from your live agent
You already built the graph in code, so let the SDK read it off the live object instead of hand-writing JSON. extract_topology walks a constructed OpenAI Agents Agent or a compiled LangGraph and returns exactly the {sub_agents, topology} shape above:
from dystopic.odyssey.topology import extract_topology
root = build_root_agent() # your entrypoint agent object
declared = extract_topology(root) # {"sub_agents": [...], "topology": {...}} — or {} for a single agentA single agent with no handoffs returns {} — nothing to declare, which is the whole point of this being optional.
The cleaner move is to skip the intermediate dict entirely and pass the live object straight to the registration helper as topology_from. It calls extract_topology internally and folds the result into the config before shipping:
from pathlib import Path
from dystopic.odyssey import registration
registration.create_code_agent(
name="support-triage",
entrypoint="run",
source_dir=Path("./agent_src"),
api_key="pk_live_...", # org API key from Settings → API Keys
topology_from=root, # live object; extract_topology runs internally
)create_code_agent is the recommended path — it registers a code (sandbox) agent and sets its topology in one call. See code mode for the sandbox default.
Passing topology_from is an explicit assertion: the extracted topology unconditionally overwrites any sub_agents / topology already on the config. Don't also hand-author those keys — let the helper own them.
Dump it for the dashboard
If you'd rather paste JSON into the dashboard's Import JSON dialog, dump it from the CLI. dystopic odyssey dump-agent imports a zero-arg factory you already wrote (module:attr), builds the object, and prints the payload; --section topology narrows the output to just sub_agents + topology:
dystopic odyssey dump-agent \
--framework openai \
--factory app.agents:build_root_agent \
--section topologySupported --framework values are openai, anthropic, langchain, and strands. Paste the output into Agent → Topology → Import JSON on the web dashboard at https://platform.pipelines.tech.
Topology comes from topology_from at registration, the dashboard editor, or a raw PUT /api/agents/{id} — there is no config file to edit. If you need the raw-API path (and its merge semantics), see the declare-world reference.
Rules worth knowing up front
- Actor labels. An
actor_id(and eachtalks_totarget) is a/-delimited label: charset[a-zA-Z0-9_.:-]per segment, at most 64 chars per segment, at most 8 segments deep, at most 256 chars total."supervisor/refund_worker"is valid. - Hub wildcard. A
talks_toof["*"]means "may reach any sub-agent" — the shortcut for a supervisor that fans out to everyone without enumerating targets. - Resolver alignment. If you pass a custom
name_to_actorresolver to map framework-native names toactor_ids, pass the same resolver to your runtime hooks. Mismatched labels show up asundeclaredeven though they're in the topology.
The declare-world reference has the full extraction, overwrite, and label-rule details.
Declare the ledger schema (simulated data only)
When your tools read and write entities the simulator invents — an order, a support ticket, an account — a ledger schema declares that closed-world ontology up front. It's the ledger_schema you set on the agent, either by passing it to create_code_agent or by importing JSON in the dashboard:
from pathlib import Path
from dystopic.odyssey import registration
ledger_schema = {
"entities": [
{
"type": "order",
"id_field": "order_id",
"description": "Customer orders",
"fields": [
{"name": "status", "type": "string",
"enum": ["pending", "shipped", "delivered"]},
{"name": "amount", "type": "number"},
],
}
],
"flags": {"policy": "open", "values": ["warehouse_outage"]},
}
registration.create_code_agent(
name="support-triage",
entrypoint="run",
source_dir=Path("./agent_src"),
api_key="pk_live_...",
tools_schema=my_tools,
ledger_schema=ledger_schema,
)To edit it later without re-authoring, dump the current shape with dystopic odyssey dump-agent --section ledger and paste it into Agent → Ledger → Import JSON on the dashboard.
Prefer not to hand-write it? Generate a draft from your tools_schema, tweak it, then pass it straight into registration:
from dystopic.odyssey import registration
draft = registration.generate_ledger_schema(
api_key="pk_live_...",
tools_schema=my_tools,
)
# tweak `draft` if you want, then ship it
registration.create_code_agent(
name="support-triage",
entrypoint="run",
source_dir=Path("./agent_src"),
api_key="pk_live_...",
tools_schema=my_tools,
ledger_schema=draft,
)generate_ledger_schema POSTs to /api/agents/ledger-schema:generate and returns the draft dict.
The rules that actually bite
These are the validation rules that turn into a 422 at registration — get them right the first time:
- At least one entity, or
null. A non-null schema must declareentitieswith at least one type. An emptyentities: []is rejected — sendledger_schema=None(the default) for the fully-open ledger instead. - Entity types are singular,
lower_snake_case. Eachtypemust match^[a-z][a-z0-9_]*$—order,order_item, neverOrderororder-item. Strict mode compares your declared type verbatim against the simulator's emittedentity_type, so a non-canonical form never matches and burns the regeneration budget. - No singular/plural collisions.
orderandordersfold onto the same canonical key and are rejected as a collision (already-persisted collisions on an existing agent are grandfathered so you can still edit unrelated fields). - Field types are a fixed set. Each field
typeis one ofstring,number,integer,boolean,object,array. - Adapters need a matching entity. A ledger adapter is a per-tool binding that maps a tool call to a deterministic state change instead of letting the simulator infer it — see when to use one. If a tool declares
ledger_write_policy: "adapter"for an entity op (add/update/remove), itsentity_typemust exist inledger_schema.entities, or registration422s. Aset_flagadapter binds no entity and is exempt.
The declare-world reference covers field_policy (open vs. closed field closure), the three-state update semantics (omit / dict / null), flag vocabularies, and the full byte-level shape. How each tool's response is produced — simulated, executed in-sandbox, or forwarded live — is a separate axis covered in execution modes.
Next
Your world is declared — or you've confirmed a single stateless agent needs no extra declaration. Either way, you're ready to write the scenarios and the gate.
4 · Create a test suite
Author scenarios in the dashboard, create a suite, bind your scenarios, and set the gate.
Declare-world reference
Topology extraction and overwrite rules, label grammar, and the full ledger ontology.
Execution modes
How each tool's response is produced: simulated, executed in-sandbox, or forwarded live.
2 · Register your agent
Register your ported agent on the platform — from the dashboard or the SDK — then publish it draft→active so checks can run it.
4 · Create a test suite
Author scenarios, a suite, and a gate on the platform, bind the suite to CI, and verify locally with dystopic ci review --platform-suite.