Skip to content

Sidecars: sharing a heavy resource across sessions

A sidecar is a long-lived helper process that a CodeExecutionServer launches at startup and shuts down on exit. It exists to hold expensive, process-global state — most often a large model — so that state is loaded once per container and shared by every kernel session over loopback HTTP.

When to use a sidecar

Each execute_{name}_code session runs in its own isolated kernel process. That isolation is a feature — sessions can't corrupt each other's state — but it means anything a tool loads is loaded per session. For a lightweight import that's fine. For a multi-gigabyte model it is not: N concurrent sessions load N copies of the model, and a worker fan-out inside each copy multiplies the cost further. On a modestly-sized host this quickly exhausts RAM and the kernel's OOM-killer starts reaping processes.

A sidecar breaks that multiplication. The model is loaded once in the sidecar; kernels stay cheap and call into it over HTTP.

Reach for a sidecar when all of these hold:

  • A resource is expensive to initialize (large model weights, a big rule base, a warm connection pool) and would otherwise be re-initialized inside every kernel session.
  • The resource is safe to share across sessions — it is read-only at inference time, or you serialize access inside the service.
  • The work is request/response shaped and fits a small HTTP surface (POST /predict, GET /health).

Do not use a sidecar for:

  • Static files (model weights on disk, reference datasets). Those are assets — declare them in ServerConfig.assets and read them from MCP_ASSET_CACHE_DIR. A sidecar is for a running process, not a file.
  • Cheap, per-call work that doesn't benefit from shared warm state.
  • Proxying other MCP servers. That is a ConnectorServer — a sidecar is a plain internal service, invisible to the agent, not an MCP endpoint.

How it works

Declare sidecars on your ServerConfig:

from agora_workbench.code_execution import ServerConfig, SidecarConfig

config = ServerConfig(
    name="retrochimera",
    description="Retrosynthesis route prediction.",
    type="conda",
    dependency_file="...",
    sidecars=[
        SidecarConfig(
            name="retrochimera-model",
            # argv appended to the kernel env's Python (use_env_python=True)
            command=["-m", "retrochimera_tools.model_service"],
            url_env_var="RETROCHIMERA_MODEL_SERVICE_URL",
            port=8901,
            health_path="/health",
            readiness_timeout_s=300.0,
        ),
    ],
)

Lifecycle, handled for you by CodeExecutionServer:

  1. Startup — after the kernel environment is built, the server launches each sidecar, then polls its health_path until it returns 2xx (or readiness_timeout_s elapses, which fails startup). Kernels are only registered after the sidecars are ready.
  2. Discovery — the sidecar's base URL (http://{host}:{port}) is exported into the server process environment under url_env_var. Because each kernel inherits the server's environment at spawn, every session sees this variable — the same mechanism used for MCP_ASSET_CACHE_DIR.
  3. Shutdown — on server exit each sidecar is stopped gracefully (SIGTERM, then SIGKILL after a grace period).

Sidecars are not started by warm() (build-time environment prep only) — they are runtime processes tied to a serving instance.

The use_env_python split

The server process runs in the base image's Python, but your tools — and usually your model — live in the per-server kernel environment. With use_env_python=True (the default), command is appended to that environment's interpreter (ServerConfig.get_python_path()), so the sidecar can import the same heavy dependencies the tools use, with no second environment to manage.

Set use_env_python=False to run an arbitrary executable verbatim instead (a standalone binary, or a service served from a different interpreter).

Writing the sidecar service

The sidecar is an ordinary HTTP server. It is told where to bind via the SIDECAR_HOST and SIDECAR_PORT environment variables the framework injects, and it must expose the health_path used for the readiness probe. Keep it on loopback — a sidecar is an internal implementation detail and must not be exposed off-box.

# retrochimera_tools/model_service.py
import os
from fastapi import FastAPI
from .model import load_model  # expensive: called exactly once here

app = FastAPI()
_model = load_model()  # loaded once for the whole container

@app.get("/health")
def health():
    return {"status": "ok"}

@app.post("/predict")
def predict(payload: dict):
    return _model.predict(payload["smiles"])

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        app,
        host=os.environ.get("SIDECAR_HOST", "127.0.0.1"),
        port=int(os.environ["SIDECAR_PORT"]),
    )

If the model is not safe for concurrent access, serialize calls inside the service (e.g. an asyncio.Lock or a single worker) rather than relying on the caller.

Calling the sidecar from tool code

Tool code reads the injected URL and talks to the sidecar over HTTP. Fall back to a direct in-process call when the variable is absent, so the same tool works in tests and local development without the sidecar running:

import os
import httpx

def predict_retrosynthesis(smiles: str) -> dict:
    base = os.environ.get("RETROCHIMERA_MODEL_SERVICE_URL")
    if base:
        resp = httpx.post(f"{base}/predict", json={"smiles": smiles}, timeout=600)
        resp.raise_for_status()
        return resp.json()
    # Dev/test fallback: load and run the model in-process.
    from .model import load_model
    return load_model().predict(smiles)

Configuration reference

SidecarConfig fields:

Field Default Description
name Logical name for the sidecar (must be unique within a server).
command Argument vector. With use_env_python=True these args are appended to the kernel env's Python; with False, executed verbatim.
use_env_python True Prepend the kernel environment's Python interpreter to command.
url_env_var Env var under which the base URL is exported to the server process (and inherited by kernels).
host 127.0.0.1 Loopback bind address. Keep internal.
port TCP port the sidecar listens on (also passed as SIDECAR_PORT).
health_path /health Path polled (HTTP GET, expecting 2xx) to determine readiness.
readiness_timeout_s 120.0 Max seconds to wait for readiness before failing startup.
env {} Extra environment variables set on the sidecar process.

Helpers: base_url() returns http://{host}:{port}; health_url() returns the full readiness URL.

Sidecar vs. a second container

A sidecar is a co-located process — same container, same kernel environment, loopback transport. Prefer it: there is no extra image to build, no cross-container networking, and it reuses the environment your tools already need.

Reach for a separate container only when you need to scale the model independently of the server, share one model instance across multiple server replicas, or give it a fundamentally different runtime (e.g. GPU scheduling). In that case point tool code at the remote service's URL directly — the sidecar abstraction is unnecessary because the framework isn't managing that process's lifecycle.