AgentCanvas / Pages / Developer Guide / Capabilities / 3Isolated Runtime Environments
2026-06-17

Research tools often require conflicting Python environments. Any BaseNodeSet can run in server mode in its own interpreter โ€” the framework auto-generates an HTTP server from the tool's port definitions. Zero extra server code. Under batch eval, the same mechanism fans out to N tagged subprocesses for per-worker isolation (ADR-eval-002).

Design docs: Server Mode ยท Env Panels


1. Problem

AgentCanvas runs in agentcanvas (Python 3.10+, modern torch / HF / FastAPI โ€” the primary env, ADR-platform-004). Research tools it drives often need different environments:

These cannot coexist in one interpreter. AgentCanvas solves this by running each nodeset in its own subprocess with its own interpreter โ€” transparently to the canvas.

2. Two Loading Modes

Mode Command Where tools run Use case
Local POST /api/components/nodesets/{name}/load In the backend process Same-env tools, fast I/O
Server POST /api/components/nodesets/{name}/load?mode=server In a separate subprocess Different Python env, GPU isolation, parallel workers

In both modes the tools appear as identical canvas nodes โ€” the researcher doesn't need to know which mode is active. Under batch eval, worker_count > 1 auto-routes env nodesets to server mode even if the graph was loaded locally (registry.py ยท load_nodeset, gated on parallelism="replicated").

3. Auto-Server Architecture

When mode=server, the framework performs these steps automatically:

1. WorkspaceComponentRegistry._load_nodeset_as_server(name, nodeset, worker_count=N)
2. Read server_python from nodeset class (or default to sys.executable)
3. Allocate N free TCP ports (one subprocess per worker)
4. Launch subprocess(es):
     PYTHONPATH=... {server_python} -m app.server.auto_host \
       --file {source_file} --class {class_name} --port {port}
5. Subprocess starts AutoServerApp:
     a. Instantiate nodeset env panel (if any) for the BaseNodeSet class
     b. Call on_startup() โ†’ instantiate nodeset, await initialize()
     c. Introspect get_tools() โ†’ read input_ports / output_ports
     d. Auto-generate ServerFunction per tool
     e. Serve HTTP: /manifest + /call/{fn} + /env-panel/* + /health
6. Framework fetches /manifest from each subprocess
7. Generate proxy canvas nodes from manifest
8. Register proxy nodes in NODE_HANDLERS (once; proxies route to the right subprocess at call time)

3.1 AutoServerApp Introspection

AutoServerApp reads each tool's PortDef declarations and converts them to ServerFunction entries:

# From auto_server_app.py โ€” get_functions()
for tool in self._nodeset.get_tools():
    functions.append(ServerFunction(
        name=tool.node_type,
        description=tool.description,
        input_ports=_portdefs_to_schemas(tool.input_ports),
        output_ports=_portdefs_to_schemas(tool.output_ports),
        config_schema=tool.config_schema,
        handler=_make_handler(tool, self._batched_server),  # batched server passed for batched=True nodes
    ))

No manual port re-declaration needed โ€” the same PortDef used for local mode drives the server API. The manifest.FunctionSchema.name is the tool's node_type, so local and server-mode proxies register under the same node_type (TODO #33 closed).

4. Proxy Nodes

After fetching the manifest, the framework generates proxy canvas nodes โ€” BaseCanvasNode subclasses that forward forward() calls to the remote server via HTTP.

Canvas node forward()  โ†’  HTTP POST /call/{fn}  โ†’  subprocess: tool.forward()  โ†’  HTTP response  โ†’  return dict

Proxy nodes have the same node_type, input_ports, output_ports, and category as the original tool. They're registered in NODE_HANDLERS exactly like local nodes, so the GraphExecutor treats them identically.

At call time, a proxy checks the running executor for a per-worker server_url_overrides entry (ADR-eval-002 PB-1.5, proxy.py::_make_forward); if present, the request is routed to that worker's URL, otherwise it falls back to the globally-registered URL.

5. Env Panel Bridging

Env nodesets own mutable episode state (current split, current episode, last observation) that must live inside the subprocess where the simulator actually runs. ADR-server-002 formalises this with a generic BaseEnvPanel contract:

This replaced the earlier pattern of smuggling episode control through hidden env_*__ep_* tool nodes. 5 hidden tools + api/canvas/env.py (233 lines) + app/env_controller.py (343 lines) were deleted; episode management is fully decoupled from the tool-call path.

6. Worker-Pool Fan-Out (ADR-eval-002)

For batch evaluation, worker_count > 1 extends server mode to N parallel isolated subprocesses per env nodeset:

ensure_nodesets_for_graph(graph, worker_count=N)
  โ”œโ”€ for env nodeset in graph:
  โ”‚    โ””โ”€ _load_nodeset_as_server(name, ns, worker_count=N)
  โ”‚         โ”œโ”€ allocate N free ports
  โ”‚         โ”œโ”€ spawn N auto_host subprocesses
  โ”‚         โ”œโ”€ fetch manifest from first; generate proxy nodes once
  โ”‚         โ””โ”€ register N tagged RemoteEnvPanelProxy instances: "{name}#0"โ€ฆ"{name}#{N-1}"
  โ””โ”€ non-env nodesets stay singleton (one subprocess, shared across workers)

EnvWorkerPool then leases one WorkerHandle per episode; each handle carries:

At worker_count=1 both overrides are empty and the code path collapses to the singleton case (zero behaviour change). On eval shutdown, all N tagged subprocesses are unloaded together with rollback if any failed to come up.

7. Batched Inference (ADR-eval-002)

Some nodes (e.g. policy_adapter_vla__predict) amortise better when K parallel callers share one GPU forward pass. BatchedInferenceServer is a per-AutoServerApp rendezvous tier:

This sits inside a single subprocess (one AutoServerApp instance), so it composes cleanly with the worker-pool fan-out: K parallel workers submit into the same batched queue hosted by the inference subprocess.

8. How to Use

8.1 Specifying a Different Interpreter

Add server_python to the nodeset class:

class SlamNodeSet(BaseNodeSet):
    name = "slam"
    description = "SLAM spatial tools"
    server_python = "/opt/ros/noetic/bin/python3"

    def get_tools(self) -> list:
        return [LocalizeTool(), MapQueryTool(), FrontierTool()]

For the ac-vlnce env:

class EnvHabitatNodeSet(BaseNodeSet):
    name = "env_habitat"
    server_python = "python"
    env_panel = HabitatEnvPanel  # ADR-server-002 โ€” lives in the subprocess

8.2 Loading via API

# Load in local mode (default)
curl -X POST http://localhost:8000/api/components/nodesets/sam/load

# Load in server mode (auto-hosted subprocess)
curl -X POST http://localhost:8000/api/components/nodesets/slam/load?mode=server

# Unload (works for all modes, including worker-pool)
curl -X POST http://localhost:8000/api/components/nodesets/slam/unload

Batch eval loads nodesets via ensure_nodesets_for_graph(graph, worker_count=N) โ€” there is no public ?worker_count=N query parameter; fan-out is driven by EvalConfig.worker_count.

8.3 Opting Into Batched Inference

class MyPolicyForward(BaseCanvasNode):
    node_type = "my_policy__forward"
    batched = True
    batch_dim = "raw_obs"  # must name an actual input port
    input_ports = [
        PortDef("raw_obs", WIRE_OBSERVATION),
        PortDef("hidden_in", WIRE_ANY),   # explicit per-caller state
        ...
    ]
    output_ports = [
        PortDef("action", WIRE_ACTION),
        PortDef("hidden_out", WIRE_ANY),  # explicit per-caller state
    ]

9. Key Files

File Role
agentcanvas/backend/app/server/auto_server_app.py AutoServerApp โ€” introspects nodeset, generates ServerFunction entries, hosts BatchedInferenceServer, instantiates nodeset env panel in-process
agentcanvas/backend/app/server/auto_host.py CLI entry point: python -m app.server.auto_host --file ... --class ... --port ...
agentcanvas/backend/app/server/server_app.py ServerApp base โ€” HTTP endpoints, manifest serving, /env-panel/* routes (ADR-server-002)
agentcanvas/backend/app/server/base_server.py BaseServer โ€” subprocess lifecycle, health check, manifest fetch
agentcanvas/backend/app/server/nodeset.py ServerNodeSet โ€” loadable group backed by an external server process
agentcanvas/backend/app/server/proxy.py generate_proxy_nodes() + _make_forward โ€” proxy BaseCanvasNode classes with per-worker URL routing
agentcanvas/backend/app/server/batched_inference.py BatchedInferenceServer + _BatchQueue โ€” per-app rendezvous batching (ADR-eval-002)
agentcanvas/backend/app/server/manifest.py ServerManifest, FunctionSchema, PortSchema โ€” manifest data model
agentcanvas/backend/app/server/serialization.py Wire-type (de)serialization for HTTP transport โ€” base64 images, numpy arrays, nested dicts
agentcanvas/backend/app/components/registry.py _load_nodeset_as_server(worker_count=N), tagged-proxy registration, rollback on partial failure
agentcanvas/backend/app/components/env_panel.py BaseEnvPanel ABC + RemoteEnvPanelProxy (ADR-server-002)
agentcanvas/backend/app/agent_loop/env_worker_pool.py EnvWorkerPool + WorkerHandle โ€” N-worker lease / release for batch eval

Status

Item Status Notes
Local loading mode Done load_nodeset(name, mode="local") โ€” in-process
Server loading mode Done load_nodeset(name, mode="server") โ€” subprocess with auto-server
AutoServerApp (introspect PortDef โ†’ ServerFunction) Done Zero manual port re-declaration
auto_host CLI (python -m app.server.auto_host) Done --file, --class, --port args
BaseServer (subprocess lifecycle, health check, manifest) Done start/stop/restart, _wait_for_health, fetch_manifest
Proxy node generation Done generate_proxy_nodes() + per-worker URL routing in _make_forward
server_python ClassVar Done Selects interpreter for subprocess
Load/unload API endpoints Done POST /api/components/nodesets/{name}/load?mode=server
Manifest protocol (/manifest endpoint) Done ServerApp serves manifest with FunctionSchema entries; node_type is the schema name
Env panel bridging (ADR-server-002) Done /env-panel/* routes, RemoteEnvPanelProxy, env panel lives in subprocess
Worker-pool fan-out (ADR-eval-002 PB) Done worker_count > 1 spawns N tagged subprocesses + N tagged proxies with rollback
Per-worker URL routing (ADR-eval-002 PB-1.5) Done server_url_overrides consulted by proxy._make_forward
Batched inference tier (ADR-eval-002 PC) Done BatchedInferenceServer + batched/batch_dim opt-in on BaseCanvasNode
Docker server mode Planned Feature TODO F7 โ€” unblocked by ADR-eval-002; subprocess spawn is the only seam to swap
AgentCanvas docs