1 · Port your agent
Wrap your existing agent as a sandbox/code agent, route its tool calls through the Odyssey proxy, and run it locally before you register it on the platform.
Porting is the one-time step that lets Dystopic drive your agent. You keep your agent's logic exactly as it is; you only change two things: how a run reaches it (an entrypoint the platform can call) and how its tools talk to the outside world (through the Odyssey proxy instead of live APIs). Once those two seams are in place, every later step — registering, running suites, gating merges — just works, because the platform can dispatch a run to your agent and watch which tools it calls.
This page is the golden path: scaffold a code agent, drop your logic into one entrypoint, re-point your tools through the proxy, and run it locally before you register it. When you need the full contract or the edge cases, follow the links into the port-agent reference.
Every agent is a code (sandbox) agent: you hand the platform your Python source, it runs your agent in an isolated E2B sandbox and calls one entrypoint function per task — no server to host, no tunnel, no inbound auth. (On the wire this ships as mode: "sandbox".)
Step 1 · Install the SDK
The proxy helper and registration commands ship in the odyssey extra of the SDK:
pip install 'dystopic[odyssey]'Importing dystopic.odyssey without this extra raises an ImportError telling you to install it — so if you see that, this is the fix.
Step 2 · Scaffold the code agent
You don't hand-write the entrypoint. The scaffolder emits a ready main.py (already wired past the two sandbox footguns) and a README:
dystopic odyssey scaffold --mode code--mode code is the default, so you can drop it. The command is filesystem-only — no network, no credentials — and safe to re-run with --force. It leaves you with a main.py whose entrypoint the sandbox will call:
def run(task_input: dict, *, proxy_url: str, run_token: str) -> dict:
# task_input : the scenario's initial state (a dict), or {}
# proxy_url : POST tool calls to {proxy_url}/tools/{tool_name}
# run_token : the per-run bearer for those calls
instruction = task_input.get("user_instruction") or ""
result = my_agent.run(instruction) # ← your existing agent logic
return {"final_response": result.output}A few facts worth knowing up front:
- The signature is fixed.
proxy_urlandrun_tokenare keyword-only — the sandbox injects them per run. The task payload arrives as the positionaltask_input(a dict, or{}). final_responseis the only required field. Return a dict with a non-emptyfinal_response(optionallymessagesandmetadata). Returning""orNoneis rejected because the platform's judge has nothing to grade — so always return at least a one-sentence summary, even on failure:return {"final_response": result.output or "Agent did not produce an output."}.- Read
main.pybefore you edit it. The scaffold's two comments — the sandbox's already-running event loop (noasyncio.run()on the main thread) and thread-local ContextVars — are the two bugs everyone hits. Both are covered in Code mode: upload source and Runtime gotchas.
The run(task_input, *, proxy_url, run_token) entrypoint, Envelope.from_env(), the 50-file / 200 KB / 1 MB source caps, and the wholesale config-replace-on-update behavior all live in Code mode: upload source. You don't need them to get a first run green.
Step 3 · Route tool calls through the proxy
This is the heart of porting. In a real deployment your tools hit live APIs; under Dystopic they must hit the Odyssey proxy instead, so the simulated world can answer them. The proxy lives at {proxy_url}/tools/{tool_name} and authenticates with the per-run run_token. The scaffold ships a dependency-free proxy_call that speaks that contract:
def get_order(order_id: str) -> dict:
# Was: return live_api.orders.get(order_id)
return proxy_call(
"get_order",
{"order_id": order_id},
proxy_url=proxy_url,
run_token=run_token,
)proxy_call(name, args, *, proxy_url, run_token) POSTs args to {proxy_url}/tools/{name}, attaches Authorization: Bearer {run_token}, and returns only the response field of the proxy's reply (not the full {"tool_name", "response", "source", ...} envelope — it unwraps that for you).
The scaffold's proxy_call takes proxy_url / run_token as explicit keyword args so it stays stdlib-only inside the sandbox. If you vendor the SDK into your upload, you can instead bind the run context once with Envelope.from_env() + set_current(...) and call the SDK's ambient proxy_call(name, args) with no plumbing — but that binding must happen on the same thread as the call. See Code mode: upload source.
The name you pass to proxy_call — e.g. "get_order" — is the registered tool name, the {tool_name} segment of the URL. It must match the tool you'll declare when you register the agent, or the proxy has nothing to route to. Keep these names aligned.
Whether the proxy simulates the response, runs your tool's real code in the sandbox, or forwards to a live endpoint is a per-tool decision made at registration — the three execution modes (Simulated / Executed / Live). Porting is the same regardless; you learn the axis in Execution modes and configure it — along with the tool's schemas, ledger write policy, and human-approval gating — when you declare the tool on the platform. The tools reference is the canonical home for that whole per-tool surface.
Step 4 · Run it locally
A code agent is plain Python, so you can prove your entrypoint end-to-end on your own machine before you ever upload it — no server, no tunnel. Call run(...) directly with a task and a real proxy URL + run token for a run (or a stub proxy for a smoke test):
from main import run
result = run(
{"user_instruction": "Refund order #4521 if it shipped > 30 days ago."},
proxy_url="https://api.pipelines.tech/api/odyssey-proxy/runs/<run>",
run_token="<per-run-bearer>",
)
assert result["final_response"] # non-empty, or the platform rejects it
print(result)Iterate on main.py until run(...) returns a non-empty final_response for your sample tasks. Set DYSTOPIC_AGENT_SDK_DEBUG=1 to see the proxy-call lines while you work. When it's green locally, you're ready to register: registration uploads this exact source tree and dispatches it in an isolated sandbox — the platform installs your requirements, injects the per-run proxy_url and run_token, and calls the same entrypoint you just ran.
The CLI talks to the API base URL https://api.pipelines.tech by default; override with --base-url or DYSTOPIC_BASE_URL if you're pointed at another environment. See Setup for how your key resolves.
What you have now
A code agent that: receives a task, reads the prompt from task_input, calls its tools through proxy_call (so the simulated world answers them), and returns a non-empty final_response — proven locally on your own machine. That is everything registration and the rest of the flow need.
Go deeper
The dispatch contract
Every Envelope field, the full response schema, the proxy request/response shape, and the auth → ping → connectivity → parse ordering the SDK implements for you.
Code mode: upload source
The run(task_input, *, proxy_url, run_token) entrypoint, Envelope.from_env(), source-file caps, the sandbox asyncio footgun, and wholesale config replace.
Execution modes
Simulated / Executed / Live — where each tool's response comes from, and how to set default_execution_mode per tool.
Runtime gotchas
Concurrency and manage_env, proxy retries, multi-agent actor attribution, and the empty-response / malformed-message failure modes.
Next
Your agent is portable and locally runnable. Now give it a home on the platform: 2 · Register your agent → registers it via the dashboard or the SDK and hands back the numeric agent id.