dystopic docs is in beta — content is actively being added.
dystopic
ReferencePort your agent

The dispatch contract

The contract every ported agent honors — the run context you receive, the response you return, the proxy tool-call format, and the tool-schema shape.

Every ported agent — the platform runs your source in a sandbox — honors one contract. The platform hands your entrypoint the run context, your agent does its work, calling tools by POSTing to the Odyssey proxy, and you hand back a response whose only required field is a non-empty final_response. This page is the field-by-field contract for that exchange.

The run context: what you receive

There is no inbound request to parse. Before calling your entrypoint, the sandbox injects the run context two ways:

  • Entrypoint argumentsrun(task_input, *, proxy_url, run_token): the scenario's task payload as the positional task_input (a dict, or {}), and the per-run proxy_url / run_token as keyword-only arguments.
  • DYSTOPIC_* environment variables — the same proxy URL, run token, and non-secret correlation ids (DYSTOPIC_RUN_ID, DYSTOPIC_TASK_ID, DYSTOPIC_RUN_TOKEN_JTI, …), so code that can't thread arguments (framework adapters, subprocesses) can rebuild the context ambiently.

Envelope.from_env() reads those variables back into the frozen Envelope dataclass the rest of the SDK (proxy_call, ScopedProxy, the framework adapters) expects. DYSTOPIC_ODYSSEY_PROXY_URL and DYSTOPIC_RUN_TOKEN are required — a missing one raises KeyError, because without them the agent can't talk back to the proxy. Everything else degrades to a safe default (user_instruction is "" and task_input is {} on the env path — those arrive via the entrypoint arguments instead).

The full variable table lives in the wire contract; run_token_jti is the safe-to-log correlation id — log it instead of hashing run_token.

The response: what you return

The platform requires exactly one fieldfinal_response — and optionally accepts messages and metadata for rich trace rendering. The minimum valid response is:

{ "final_response": "The refund of $79.50 has been issued for order #4521." }

The SDK's build_response coerces three return shapes into that body, so a handler can return naturally:

  • A str → wrapped as {"final_response": <str>}.
  • A dict → must contain final_response; optional messages and metadata pass through, and any other keys pass through untouched (the platform ignores unknown keys).
  • An object with a final_response, final_output, output, or message attribute (in that probe order) → the attribute is extracted and wrapped. This covers most framework result types (OpenAI Agents RunResult, Anthropic Message, Strands AgentResult) so you can return result directly.

final_response must be a non-empty string. Returning "" (or a dict/object whose final_response coerces to empty) raises ResponseShapeError and the run fails. The platform rejects empty responses because the LLM judge has nothing to grade against. Always return at least a one-sentence summary — even on failure:

return result.output or "Agent did not produce an output."

A dict return that omits final_response entirely, or an object with none of the four recognized attributes, also raises ResponseShapeError. Numbers and other non-None values under final_response are coerced to str rather than rejected; only empty and None fail.

The optional messages and metadata are for rich rendering only — the run is still graded on final_response. If either is malformed, the platform drops it to null with a soft_warnings entry and grades on final_response alone; no exception is raised on your side (see gotchas).

The proxy tool-call format

Your agent doesn't execute tools itself — it forwards each call to the Odyssey proxy, which routes it to the simulator (or a live endpoint) for this run. The contract is one POST:

POST {proxy_url}/tools/{tool_name}
Authorization: Bearer {run_token}
Content-Type: application/json

{ "order_id": "4521" }          ← the tool's argument object

The request body is the tool arguments object (validated by the proxy against the tool's input_schema). A successful response is an envelope:

{
  "tool_name": "get_order",
  "response": { "status": "shipped", "shipped_at": "2026-04-01" },
  "source": "odyssey",
  "latency_ms": 412,
  "matched_rule_index": null
}

Extract response only — not the whole envelope. The tool_name / source / latency_ms fields are plumbing; handing them back to your model pollutes the prompt with platform internals. The SDK's proxy_call/async_proxy_call do this unwrapping for you: they return payload["response"] and nothing else.

proxy_call(name, args) reads the active envelope off the ambient ContextVar (bound with Envelope.from_env() + set_current(...)) and issues exactly this POST, so a module-level tool body reduces to one line:

from dystopic.odyssey import proxy_call

def get_order(order_id: str) -> dict:
    return proxy_call("get_order", {"order_id": order_id})

On a 4xx/5xx (or a network failure) the SDK raises ProxyCallError, carrying status_code, the parsed body, and a structured error_class pulled from detail.error_class — branch on error_class, not on English message strings. Retry policy, terminal statuses, and multi-agent X-Pipelines-Actor-Id attribution are covered in gotchas.

The tool-schema shape

When you register an agent, you declare its tools as a tools_schema list. Each entry is validated by the platform at registration:

{
  "name": "get_order",
  "input_schema": {
    "type": "object",
    "properties": { "order_id": { "type": "string" } },
    "required": ["order_id"]
  },
  "description": "Look up an order by ID",
  "output_schema": {
    "type": "object",
    "properties": { "status": { "type": "string" } },
    "required": ["status"]
  },
  "default_execution_mode": "sandbox"
}
  • name (required) — non-empty, unique within the list, matching ^[A-Za-z_][A-Za-z0-9_\-]{0,127}$ (OpenAI/Anthropic-compatible naming).
  • input_schema (required) — a JSON Schema object. Parsed permissively, but must be an object under the platform's max nesting depth.
  • description (optional) — string.
  • output_schema (optional) — a JSON Schema object. Absent means the simulator's generated response may not match your tool's real shape — declare one when your agent indexes structurally into results (see gotchas).
  • default_execution_mode (optional, defaults to Simulated) — accepts either the public vocabulary "simulated" | "executed" | "live" or the legacy stored values "sandbox" | "code_intercepted" | "passthrough" (still accepted; they're what the platform persists on the wire). "simulated" (stored "sandbox") has the world engine invent the response, "executed" (stored "code_intercepted") runs your real tool code in-sandbox with data routed to the ledger, and "live" (stored "passthrough") forwards to a bound live endpoint (requires a passthrough_binding). Absent or null both mean Simulated. See execution modes for the full axis and the public↔wire mapping.

The SDK's Tool(...).to_dict() emits exactly this shape; simulated(), executed(), and live() set default_execution_mode for you (intercepted() and passthrough() remain as legacy aliases of executed() and live()).

The fields above are the contract-relevant core. The full entry carries more — the passthrough_binding for Live tools, the ledger write policy and adapters, and requires_human_approval — all documented in the tools reference.

Next

You have the contract. Now wire it into a running agent: