dystopic docs is in beta — content is actively being added.
dystopic
Reference

Wire contract & errors

The exhaustive low-level contract every ported agent honors — response shape, proxy request/response/error classes, retry policy, DYSTOPIC_* env vars, and the SDK error hierarchy.

This is the exhaustive, byte-level contract shared by three steps of the flow: porting an agent, running checks, and reviewing findings. Those pages describe what to do; this page is the single lookup surface for exactly what goes on the wire — the shape you return, the proxy request/response/error classes, the environment variables injected into the sandbox runtime, and the SDK's exception hierarchy.

Everything here is confirmed against the SDK modules dystopic.odyssey, dystopic.errors, and dystopic.config.

Response shape (what your entrypoint returns)

The platform requires exactly one field — final_response — and optionally accepts messages and metadata for rich-mode trace rendering. build_response coerces three return shapes:

  1. str → wrapped as {"final_response": <str>}.
  2. dict → must contain a final_response key; optional messages / metadata are forwarded, other keys pass through (the platform ignores unknown keys).
  3. Any object with a final_response, final_output, output, or message attribute (in that probe order) → the attribute is extracted and wrapped. This covers the OpenAI Agents SDK RunResult, Anthropic Message, Strands AgentResult, etc., so you can return result directly.

final_response is required and non-empty

An empty or None final_response raises ResponseShapeError and fails the run — an honest failure is better than silently getting graded on nothing. The error message is:

Handler returned an empty string for final_response. The platform rejects empty responses (the LLM judge has nothing to grade against). Return at least a one-sentence summary of what the agent did or why it failed.

Always return at least a one-sentence summary, even on the failure path:

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

final_response is capped at 50,000 characters on the platform side; overflow is truncated with a …[truncated] marker and recorded as a soft-warning — the judge still scores the head.

Rich mode: messages and metadata

Both are optional. When present, they drive full-transcript trace rendering instead of the thin final-response view.

{
  "final_response": "The refund has been issued.",
  "messages": [
    { "role": "user", "content": "Refund order #4521..." },
    {
      "role": "assistant",
      "tool_calls": [
        { "id": "c1", "name": "get_order", "arguments": { "order_id": "4521" } }
      ]
    },
    { "role": "tool", "tool_call_id": "c1", "content": "{\"status\":\"shipped\"}" },
    { "role": "assistant", "content": "The refund has been issued." }
  ],
  "metadata": {
    "model": "claude-sonnet-4-5",
    "system_prompt_id": "refund-v3",
    "total_input_tokens": 1842,
    "total_output_tokens": 217,
    "agent_runtime_ms": 4815
  }
}

messages[] schema (validated on the platform side):

  • role — required, must be one of system, user, assistant, tool. Any other value drops that message.
  • content — optional; must be a string, an array, or null (oneOf: [string, array, null]). A bare content-part object is rejected.
  • tool_call_id — optional string.
  • tool_calls — optional array. Each entry normalizes to the flat form {"id"?, "name": str, "arguments"?}. The OpenAI Chat Completions nested shape ({"type": "function", "function": {"name", "arguments"}}) is accepted and flattened.
  • thinking — optional array of objects.
  • Any other key passes through opaquely (additionalProperties: true).

metadata recognised keys get first-class chrome; the rest is stored opaquely: system_prompt_id, model, total_input_tokens, total_output_tokens, agent_runtime_ms.

Soft-warnings (malformed rich mode is dropped, never fatal)

If messages or metadata is unparseable, the platform does not raise. It:

  1. Drops the offending field (or entry) to null,
  2. Appends a human-readable reason to soft_warnings,
  3. Still grades the run on final_response alone, rendering the thin trace.

Examples of what triggers a soft-warning: messages not a list, a message that isn't an object, an out-of-set role, a non-string tool_call_id, metadata that isn't an object. If every message is dropped (or you sent messages: []), the messages field normalizes to null and rich mode stays off. Write defensively, or return only final_response when unsure — the failure surfaces in the run's metadata, not as an exception in your process.

A structurally-unusable response (not merely a malformed optional field) raises AgentResponseContractError on the platform: the run is marked FAILED and the judge is not invoked. That is distinct from a soft-warning, which is advisory.

Proxy call contract

Every ported tool body becomes one HTTP call to the per-run Odyssey proxy. dystopic.odyssey.proxy_call is the smallest function that satisfies it.

Request

POST {proxy_url}/tools/{tool_name}
Authorization: Bearer {run_token}
Content-Type: application/json
X-Pipelines-Actor-Id: {actor_id}   # optional — multi-agent attribution only

The body is the tool's argument object (validated against the tool's input_schema on the platform side):

{ "order_id": "4521" }

The X-Pipelines-Actor-Id header is stamped only when an acting sub-agent label is bound (via set_actor_id, ScopedProxy, or envelope.for_actor(...)); absent, single-agent calls are byte-identical to the pre-multi-agent shape. Attribution rides the header, never the body — a tool that legitimately takes an actor_id argument passes it through unchanged.

Success response

The proxy always returns HTTP 200 for application-level outcomes (even an injected error payload — this mirrors real tool semantics), with this body:

{
  "tool_name": "get_order",
  "response": { "status": "shipped", "shipped_at": "2026-04-01" },
  "source": "odyssey",
  "latency_ms": 412,
  "matched_rule_index": null,
  "validation": null
}
  • response — the payload your tool body should consume. proxy_call unwraps and returns only this field, not the whole envelope. (Legacy paths that return the payload directly are tolerated.)
  • source — where the response came from (see below).
  • latency_ms — float.
  • matched_rule_indexint only when a failure rule fired, else null.
  • validation — object only on source: "odyssey", else null.

source values

source is the runtime provenance of one proxy response — where this call's payload actually came from. It is a wire-level field, distinct from (but downstream of) the tool's declared execution mode (Simulated / Executed / Live). A Simulated tool yields odyssey (or injected when a failure rule fires); a Live tool yields passthrough (or transport_error on a failed live hop). An Executed tool's own code runs in the sandbox, so its data-plane reads/writes come back through the data call, not this source axis. The wire strings themselves keep the legacy vocabulary — only the human-facing mode names changed.

sourceMeaning
odysseySimulator generated the response (Simulated tool — legacy sandbox).
injectedA declared failure/injection rule produced the payload.
passthroughA live endpoint was hit and returned successfully (Live tool).
errorSandbox dispatch itself errored (an {"error": ...} payload).
transport_errorA Live hop failed (endpoint unavailable / unsupported / the live tool errored).

The two error sources are intentionally disjoint: Simulated rows emit odyssey / injected / error; Live rows emit passthrough / transport_error.

For the full public↔wire mapping (simulated → sandbox, executed → code_intercepted, live → passthrough), the simulated()/executed()/live() SDK helpers, and their legacy aliases, see Execution modes.

Error responses and ProxyCallError

HTTP 4xx/5xx (or a network failure) raises dystopic.odyssey.ProxyCallError. The platform's structured error body is:

{ "detail": { "error_class": "run_token_expired", "message": "The run token has expired." } }

ProxyCallError exposes:

AttributeContents
status_codeHTTP status, or None for transport-level failures (DNS, refused, timeout).
bodyParsed JSON body, or raw text if not JSON, or None for transport failures.
tool_nameThe tool being called.
error_classThe platform's structured discriminator, extracted from body["detail"]["error_class"]. None for transport failures and legacy responses. Branch on this, not on English strings — the platform keeps the discriminator stable across releases.

Proxy HTTP-level status codes: 400 malformed tool name/args; 401 missing/invalid/expired run token; 404 valid token but run not registered in this proxy's context store (body carries detail.error_class); 500 server misconfiguration or unhandled dispatch exception; 503 backing store unreachable (detail.error_class = "context_store_unavailable").

StaleRunTokenError is a ProxyCallError subclass raised specifically when the run has closed: HTTP 401 with error_class in {run_token_expired, run_token_invalid, run_token_rejected}. The helper is_stale_run_token(exc) returns True for exactly that condition.

Retry policy

proxy_call / async_proxy_call (and data_call) auto-retry transient platform conditions:

  • HTTP 429 (rate-limited), OR
  • HTTP 503 with error_class in {lock_contention, context_store_unavailable, trace_sequence_contended}.

Everything else — 401, 404, 400/422, 500, and unrecognised 503s — is terminal and raised immediately.

  • Up to 4 total attempts (3 retries).
  • Exponential backoff with ±25% jitter, base 0.5 s, capped at 4.0 s: min(0.5 · 2^attempt, 4.0) · (0.75 + random·0.5).
  • Disable per call with auto_retry=False.
  • Override the per-call timeout with timeout=<seconds>; otherwise the default is read from DYSTOPIC_PROXY_TIMEOUT_S (falling back to 120.0 s).
from dystopic.odyssey import proxy_call

# default: retry transient errors, 120s (or DYSTOPIC_PROXY_TIMEOUT_S) timeout
result = proxy_call("get_order", {"order_id": "4521"})

# latency-sensitive: no retry, tighter timeout
result = proxy_call("get_order", {"order_id": "4521"}, auto_retry=False, timeout=30.0)

DYSTOPIC_* environment variables

The platform injects the run context into the sandbox as environment variables before calling your entrypoint. The same variables are set by make_subprocess_env(envelope) when your entrypoint spawns a subprocess that needs the run context.

VariableContentsRequired for Envelope.from_env()?
DYSTOPIC_ODYSSEY_PROXY_URLBase proxy URL (append /tools/{name}).Yes
DYSTOPIC_RUN_TOKENPer-run bearer.Yes
DYSTOPIC_API_URLOrigin (scheme://host[:port]) derived from the proxy URL.No
DYSTOPIC_RUN_TOKEN_JTINon-secret correlation id.No (optional)
DYSTOPIC_AGENT_IDAgent id. Omitted (not blanked) when the envelope has none, so the platform-injected value survives on the code-mode path.No
DYSTOPIC_TASK_IDTask id.No (optional)
DYSTOPIC_RUN_IDRun id.No (optional)

Envelope.from_env() reads DYSTOPIC_ODYSSEY_PROXY_URL + DYSTOPIC_RUN_TOKEN (both required — a missing one raises KeyError) and the optional correlation ids DYSTOPIC_RUN_TOKEN_JTI, DYSTOPIC_RUN_ID, DYSTOPIC_TASK_ID. Its user_instruction is "" and task_input is {} — those arrive to code-mode entrypoints as the task_input positional argument, not via the env.

from dystopic.odyssey import Envelope, proxy_call_with

env = Envelope.from_env()
order = proxy_call_with(env, "get_order", {"order_id": "4521"})

DYSTOPIC_PROXY_TIMEOUT_S (proxy-call default timeout) is read by the SDK but not set by the dispatch machinery — your process inherits whatever is in the environment, or the built-in 120.0 s default. An unparseable value logs a warning and falls back to 120.0 s.

SDK API error hierarchy

Calls made through DystopicClient (registration, review creation, run export — everything that hits api.pipelines.tech) raise exceptions from dystopic.errors. This is a different hierarchy from ProxyCallError above: ProxyCallError is the tool-call wire; the classes below are the platform REST API wire.

DystopicError                    (base — dystopic.DystopicError)
├── DystopicTimeoutError         (an SDK polling helper timed out)
├── DystopicConnectionError      (cannot connect to the API)
└── DystopicAPIError             (non-2xx response; carries .status_code, .message, .body)
    ├── AuthenticationError       401  (missing / invalid credentials)
    ├── ForbiddenError            403
    ├── NotFoundError             404
    ├── ConflictError             409  (typically idempotency-key reuse with a different payload)
    ├── ValidationError           422
    ├── RateLimitError            429  (carries .retry_after: float | None)
    └── ServerError               5xx  (non-retryable server errors)

error_for_status(status_code) picks the most specific subclass: the exact-status map above for 401/403/404/409/422/429, ServerError for any 500–599, and the base DystopicAPIError otherwise.

  • DystopicAPIError carries status_code: int, message: str, and body: Any; its string form is Dystopic API error {status_code}: {message}.
  • RateLimitError additionally carries retry_after: float | None.
from dystopic.errors import DystopicAPIError, RateLimitError

try:
    client.create_review(...)
except RateLimitError as exc:
    wait = exc.retry_after or 30.0
    ...
except DystopicAPIError as exc:
    print(exc.status_code, exc.body)

Anti-duplication note

This page is the single home for the low-level contract. The flow and reference pages that touch it link down here rather than restating it: