AgentCanvas / Pages / Developer Guide / Design Docs / Surfaces / Coding-Agent Backend Surface
2026-06-17

The interface a coding agent (Claude Code, Cursor, a custom harness) uses to drive AgentCanvas without the UI โ€” run experiments, edit graphs, introspect components, read results. It is three layers at once: an HTTP API, a set of canonical filesystem paths, and a process model the agent must supervise. This page tells the integration story that connects them; for any one component's internals, follow its linked design doc. An MCP server already wraps the core of this surface as discoverable tools (ยง6) โ€” bare HTTP + files remain the substrate underneath.


1. What this surface is

AgentCanvas is a typed graph DSL: an agent is a directed graph of typed Python nodes evaluated by a shared executor against a shared environment substrate (see Graph System, Graph Executor, Wire Types). Humans operate it through the desktop UI; a coding agent operates the same surface programmatically โ€” same graphs, same executor, same evaluation, with the UI replaced by HTTP calls and file edits. That surface has three layers, and one task touches all three:

coding agent HTTP API eval ยท graphs ยท components HTTP client Filesystem workspace/ ยท outputs/ reads / edits files Process model supervises

The three are not independent โ€” running an experiment POSTs to start, writes workspace/graphs/ to revise, and reads outputs/eval_runs/ to analyse. An agent that ignores any one of them round-trips through the human.

The surface was designed for the UI first, then opened to programmatic use. Some conveniences an agent expects โ€” tool discovery, schema introspection, structured auth โ€” were absent in the raw HTTP surface; the shipped MCP server (ยง6) closes the first two for its tool set, and ยง5 lists what remains.


2. Surface inventory

2.1 HTTP endpoints

All endpoints live under the FastAPI app in agentcanvas/backend/app/main.py, mounted by the routers in app/api/. Default backend port is 8000 for the user's primary instance; coding-agent skills self-host on 8765-8769 to avoid collisions.

2.1.1 Eval execution

The minimum a coding agent needs to run an experiment.

Method + Path Body / Query Returns Source
POST /api/eval/v2/start StartEvalV2Request {run_id, status} โ€” "queued" on the default subprocess path, else "pending" eval.py:164
GET /api/eval/v2/status โ€” active-run summary (status, completed_count, total_episodes, elapsed_sec) eval.py:415
GET /api/eval/v2/export/{run_id} โ€” full per-episode results + aggregate_metrics (live run in-memory, else summary.json) eval.py:522
GET /api/eval/v2/runs ยท /runs/{run_id} โ€” list past runs / one run summary eval.py:433,439
GET /api/eval/v2/episodes ?run_id per-episode rows for a run eval.py:424
POST /api/eval/v2/stop โ€” best-effort halt of the in-process (legacy) run eval.py:404
GET /api/eval/v2/queue ยท POST /runs/{run_id}/cancel โ€” JobScheduler queue snapshot / cancel a queued-or-running subprocess job (the default path โ€” /stop does not reach these) eval.py:457,466
DELETE /api/eval/v2/runs/{run_id} โ€” delete a run's outputs/eval_runs/ dir eval.py:448
POST /api/eval/v2/introspect {graph_name} graph's env nodeset, dataset/split options, episode count โ€” without running eval.py:481
WS /ws โ€” live progress events (mounted at the root, not under /api/eval/v2) websocket.py:16

The StartEvalV2Request body (eval.py:37-96). Only graph_name is required โ€” every other field has a default, so a one-line {"graph_name": "..."} is a valid run:

Field Type (default) Notes
graph_name str (required) Resolves against workspace/graphs/{graph_name}.json
episode_count int (-1) How many episodes; -1 = all
start_episode_index int (0) Consecutive-mode start index (ignored when episode_indices is set)
episode_indices list[int] | null Explicit indices โ€” overrides consecutive mode
worker_count int (1) Concurrent workers (subject to the nodeset's parallelism mode)
step_budget int | null Per-episode iteration cap. null defers to the resolver chain: env-supplied (env panel on_load) โ†’ graph-authored โ†’ DEFAULT_STEP_BUDGET. (Legacy max_steps is accepted and mapped here.)
per_step_budget_sec float | null Per-step wall-clock budget; null = the nodeset default
selectors ยท episode_selectors dict ({}) ยท list[dict] | null The generic env-panel cascade pushed before each episode (e.g. {"task_id": โ€ฆ}); episode_selectors gives per-episode overrides for cross-task sweeps. dataset and split (default "val_unseen") are legacy shims merged into this dict.
via_subprocess bool (true) Default true โ€” the run is admitted through the JobScheduler and executes in its own subprocess (returns status: "queued", a timestamp run_id). Set false for the legacy in-process path (ExecutionGuard, an 8-char hex run_id).
marginal_vram_mb ยท exclusive_gpu ยท priority int (0) ยท bool (false) ยท str ("normal") GPU-admission declaration consumed by the JobScheduler (subprocess path only).
active_workspace_dir str | null Absolute path to overlay workspace/graphs/ + graph_nodes/ before the frozen base (ADR-components-009) โ€” the mechanism architect skills use to isolate concurrent edits.

2.1.2 Graph CRUD

Used when the coding agent edits a graph between iterations. All graph routes live in app/api/canvas/graphs.py (mounted at /api/graphs). The id is a {graph_id:path} โ€” a graph name may contain / (e.g. experiments/foo), so it is hierarchical, not a flat name.

Method + Path Purpose Source
GET /api/graphs List all saved graphs (+ /folders for the folder tree) graphs.py:265,193
GET /api/graphs/{graph_id:path} Read full graph JSON graphs.py:326
POST /api/graphs ยท PUT /api/graphs/{id} Create / overwrite a graph graphs.py:279,336
DELETE /api/graphs/{id} ยท POST /{id}/move ยท POST /layout Delete, move/rename, auto-layout graphs.py:388,304,297
POST /api/graphs/validate Schema-check a graph without running graphs.py:350
POST /api/components/reload Hot-reload nodesets after editing workspace/nodesets/*.py (under the components router, not graphs) app/api/platform/components.py:96

2.1.3 Component introspection

Method + Path Purpose
GET /api/components Registered nodesets + nodes (two keys; no "policies" key) โ€” components.py:86
GET /api/components/node-schemas All node types with input/output ports + config schema (there is no per-type /nodes/{type} route) โ€” components.py:221

These are what a coding agent reads to know what wires can connect to what โ€” the runtime equivalent of the per-tutorial skill-canvas-node.md documentation. Several other live routers exist that a supervising agent may touch โ€” /api/profiles (LLM/run config), /api/env-panels (per-nodeset control panels), /api/logs, /api/config โ€” see main.py:214-228 for the full mount list.

2.1.4 Health / lifecycle

Method + Path Purpose
GET /health Liveness probe (used during self-host startup)

2.2 Filesystem conventions

Several paths are part of the contract โ€” the backend reads them as-is and the coding agent is expected to write them in canonical format.

Path Role Schema authority
workspace/graphs/{name}.json Saved graph definitions Graph System โ€” GraphDefinition
workspace/architect/exp_profiles/{name}.yaml Per-graph experiment profile (split, worker_count, primary_metric, secondary_metrics) .claude/architect/README.md ยง Per-graph experiment profile
workspace/nodesets/*.py Local nodesets (run in backend process) NodeSets
workspace/nodesets/{role}/*.py with server_python set Server-mode nodesets (run as auto_host subprocesses; role directories per ADR-platform-008) Server Mode
outputs/eval_runs/{run_id}/ Run root โ€” summary.json (metrics + episodes[]), graph.json (frozen snapshot), spec.json, shared_urls.json, stdout.log, stderr.log, _DONE Batch Eval
outputs/eval_runs/{run_id}/episodes/ep{NNNN}/ Per-episode dir โ€” log.jsonl (one row per node firing), assets/ (image/depth/tensor artifacts), episode.json. There is no top-level log.jsonl under the run root. Execution Logs (ADR-eval-004)

The path layout is stable enough that a coding agent should treat it as part of the API; re-layout changes get a corresponding ADR. Note exp_profiles/*.yaml (experiment profiles, a file convention) is unrelated to the /api/profiles endpoint (LLM/run config profiles) โ€” same word, different concept.

2.3 Process model

A live AgentCanvas backend is not a single process. When a graph references a nodeset declared parallelism = "replicated" (e.g. env_mp3d), the backend spawns one auto_host subprocess per worker; with K workers a 50-worker run is 1 (uvicorn) + 50 (auto_host) = 51 processes. Each auto_host is a small uvicorn hosting the nodeset's BaseServer (see Server Mode), receiving /call/<node_type> POSTs over loopback HTTP.

coding agent setsid -f conda run โ†’ uvicorn one backend, the PGID leader auto_host #0 โ€ฆ auto_host #N-1

Each auto_host calls os.setsid() into its own group via the _preexec_setsid_pdeathsig spawn hook (app/server/base_server.py ยท _preexec_setsid_pdeathsig), so cleanup is a PGID kill of the conda/uvicorn leader plus a per-auto_host PGID kill.

Implications for a coding agent acting as supervisor:

  1. Self-host on a dedicated port from a 5-slot pool. Reuse-by-default would mean spawning into the user's primary backend on :8000, where their kill/restart cycle could take down a mid-run experiment. Convention: walk ports 8765-8769 and acquire the first one whose lockfile under ~/.cache/agentcanvas-mcp/locks/{port}.lock is flock-able (LOCK_EX | LOCK_NB); the kernel auto-releases the lock when the holder process dies any way (clean exit, SIGTERM, SIGKILL, OOM). Each Claude Code conversation thus gets its own backend without colliding with other concurrent conversations.
  2. Spawn under setsid -f. The resulting process tree has conda run โ†’ uvicorn โ†’ many auto_host children, with each auto_host calling os.setsid() to escape the parent's group (base_server.py:52). Without setsid -f on the spawn, a single kill -- -PGID cleanup misses every auto_host. With it, one PGID kill atomically reaps the conda wrapper and uvicorn; a separate per-auto_host PGID kill reaps the rest. See /architect:experiment-http step 3 for the canonical sequence (the -mcp skill delegates this to BackendManager in agentcanvas/mcp_server/).
  3. Cleanup is mandatory and PGID-targeted. Never use pkill -f uvicorn or pkill -f auto_host โ€” the user almost always has unrelated uvicorn instances running (e.g. their primary backend on :8000, the docs site on :8001). Pattern-broad kills destroy their work.

3. Workflows

Each subsection below threads endpoints, files, and the process model into a single coherent task. Snippets are real (taken from the most recent NavGPT-MP3D experiments under outputs/design_runs/navgpt_mp3d/v2/).

3.1 Launch a batch experiment

The most common workflow.

# 1. Pick a free port + spawn backend
PORT=8765
( cd agentcanvas/backend && \
  setsid -f conda run -n agentcanvas --no-capture-output \
    uvicorn app.main:app --host 127.0.0.1 --port $PORT \
    < /dev/null > /tmp/backend.log 2>&1 )

# 2. Wait for /health
until curl -s http://127.0.0.1:$PORT/health | grep -q ok; do sleep 2; done

# 3. POST the eval config
curl -s -X POST http://127.0.0.1:$PORT/api/eval/v2/start \
  -H 'Content-Type: application/json' \
  -d '{
    "graph_name": "navgpt_mp3d",
    "split": "val_unseen",
    "episode_count": 50,
    "start_episode_index": 0,
    "episode_indices": [26, 102, 108, ...],
    "worker_count": 25,
    "step_budget": 15,
    "per_step_budget_sec": 120
  }'
# โ†’ {"run_id": "20260613_145434", "status": "queued"}

# 4. Poll status until completed (or stream via WebSocket)
curl -s http://127.0.0.1:$PORT/api/eval/v2/status

# 5. Export
curl -s http://127.0.0.1:$PORT/api/eval/v2/export/20260613_145434 > export.json

# 6. Cleanup (PGID-targeted)
kill -- -$BACKEND_PGID

The skills /architect:experiment-http (curl-based) and /architect:experiment-mcp (MCP-tool-based) wrap these steps with iter-directory bookkeeping, profile resolution, and token-cost estimation. Bare curl / MCP tools are the foundation; the skill is the convention.

3.2 Modify a graph

When the coding agent decides to revise a graph between iterations.

# 1. Read current graph
curl -s http://127.0.0.1:$PORT/api/graphs/navgpt_mp3d > graph.json

# 2. Edit graph.json directly (file-level edit; no special endpoint)

# 3. Validate before saving
curl -s -X POST http://127.0.0.1:$PORT/api/graphs/validate \
  -H 'Content-Type: application/json' \
  -d @graph.json

# 4. Save
curl -s -X PUT http://127.0.0.1:$PORT/api/graphs/navgpt_mp3d \
  -H 'Content-Type: application/json' \
  -d @graph.json

Validation catches schema breakage (missing ports, bad node types) before a real run.

3.3 Modify a nodeset (hot reload)

When the coding agent edits Python in workspace/nodesets/.

# 1. Edit workspace/nodesets/{nodeset}.py directly

# 2. Hot-reload โ€” backend re-imports the changed modules
curl -s -X POST http://127.0.0.1:$PORT/api/components/reload

Hot reload re-imports only local-mode nodesets. Server-mode nodesets (those with server_python set) require restarting the backend โ€” their code lives in already-spawned auto_host subprocesses.

3.4 Debug a single episode

The smoke-test pattern: same eval/v2/start endpoint, but episode_count=1, worker_count=1. The skill /architect:debug wraps this with a fix-loop (โ‰ค5 fix attempts, edit-whitelist constrained to the graph's nodesets); see its source for the full algorithm.

3.5 Reuse run history

# List all past runs
curl -s http://127.0.0.1:$PORT/api/eval/v2/runs

# Inspect a specific run (without re-running)
curl -s http://127.0.0.1:$PORT/api/eval/v2/runs/20260613_145434

# Read its raw log
cat outputs/eval_runs/20260613_145434/episodes/ep0000/log.jsonl | head

Old runs persist indefinitely under outputs/eval_runs/. The coding agent typically re-reads log.jsonl for analysis (per-step traces, per-node timing, LLM prompts/responses) rather than re-running.


4. Persistence and observability

4.1 Where each kind of output lands

Artifact Location Schema authority
Per-node execution trace outputs/eval_runs/{run_id}/episodes/ep{NNNN}/log.jsonl (per episode) Execution Logs
Large tensor / image assets outputs/eval_runs/{run_id}/episodes/ep{NNNN}/assets/ (per episode) Execution Logs
Run summary + graph snapshot outputs/eval_runs/{run_id}/summary.json, graph.json (run root) Batch Eval
Aggregate / per-episode metrics GET /api/eval/v2/export/{run_id} โ†’ aggregate_metrics / episodes[*].metrics Batch Eval
Live progress WS /ws (root-mounted) app/api/execution/websocket.py:16

4.2 What log.jsonl looks like

One JSON object per line, one line per node firing:

{
  "timestamp": "2026-04-30T09:55:03.217",
  "execution_id": "20260613_145434_ep0000",
  "step": 0,
  "node_id": "orchestrator_llm",
  "node_type": "llmCall",
  "duration_ms": 2843.5,
  "inputs": {...},
  "outputs": {"response": "Action: action_maker..."},
  "inner_log": [
    {"key": "rendered_prompt", "value": "..."},
    {"key": "model", "value": "gpt-4"},
    {"key": "response_length", "value": 84}
  ],
  "error": null
}

The inner_log array is where individual nodes attach their per-call breadcrumbs โ€” including the per-node usage bucket ({calls, tokens, usd_cost, model}), which now lands here as a {"key": "usage", โ€ฆ} entry (ยง5.4). Each line also carries parent_node_id / dynamic_index provenance for dynamic-fan-out children.

4.3 Live progress

For UIs and long-running supervision, the root-mounted WS /ws streams per-episode progress โ€” the executor broadcasts eval_episode_done (eval_batch.py ยท _broadcast_episode_done); the socket itself only handles inbound ping/pong (websocket.py). Coding-agent skills today poll GET /status instead, which is fine for โ‰ค60-min runs but trades immediacy for simplicity.


5. Current limitations

The surface works, but it carries assumptions from its UI-first origin. A coding agent integrating against it today encounters the following:

5.1 No tool discovery or schema introspection at the protocol level

The raw HTTP surface does not advertise its endpoints/schemas (no OpenAPI listing surfaced at a known path for agent consumption); there is a GET /api/components/node-schemas endpoint for node introspection, but no equivalent for API endpoint introspection. The shipped MCP server (ยง6) closes this for its tool set โ€” agents that go through MCP get a discoverable, JSON-Schema-validated tool list. Agents driving bare HTTP must still be told the API out-of-band, today via skill-file conventions like .claude/commands/architect/experiment.md, or read backend source.

5.2 No structured authentication or sandbox

The HTTP surface trusts loopback; there is no per-call authorisation, no per-graph quota, no execution sandbox between graphs. Acceptable for single-user development; insufficient for hosting multiple isolated coding-agent harnesses simultaneously, or for accepting a graph from an untrusted source.

5.3 Self-host process management is manual

Picking a port, spawning under setsid -f, recording the PGID, walking auto_host descendants on cleanup โ€” a curl-based skill must reimplement this dance, and any small mistake (plain setsid without -f, or pkill -f uvicorn) damages the user's environment. The MCP server's BackendManager (agentcanvas/mcp_server/) now bundles "spawn, health-check, capture cleanup handle" into one operation for the MCP path; the bare-HTTP path still has no such helper.

5.4 Token usage is now captured per node firing (ADR-observability-005)

Closed (ADR-observability-005). Token usage and dollar cost are tracked per node firing via the _current_node_usage ContextVar in app/llm/call.py:47: the executor sets a fresh accumulator dict around every node.forward() call (graph_executor.py ยท _fire_node), and every llm_complete / llm_complete_n / vlm_complete call writes its response.usage + litellm.completion_cost() into the bucket. The executor then emits one log entry per node with {calls, prompt_tokens, completion_tokens, total_tokens, cached_tokens, usd_cost, model} attached.

Coding agents reading log.jsonl for cost should consume the executor-emitted usage record directly rather than re-tokenising with tiktoken. The local-tokenise path in older /architect:experiment-* skills is now redundant; remove it in any new skill you write. See LLM Config ยง12 for the contract.

5.5 Run-directory bookkeeping lives in skills, not the backend

The backend writes outputs/eval_runs/{run_id}/ (executor-owned). The coding-agent skills add a parallel outputs/design_runs/{graph}/vN/iter_M/ tree (skill-owned, captures graph snapshots, profile snapshots, metric summaries, trace.md, debug logs). Two trees that record overlapping information is a transitional state โ€” the skill tree exists because the backend has no concept of an "iteration."

These items are the current friction points of the bare-HTTP path. The MCP layer below was the planned answer to the first three โ€” and it has shipped.


6. MCP packaging

An MCP server ships at agentcanvas/mcp_server/ (FastMCP, stdio transport). It exposes the surface as discoverable, JSON-Schema-validated tools, replacing the curl-over-self-hosted-port pattern. The MVP is 6 tools: eval_start, eval_status, eval_export, eval_stop, graph_list, eval_runs_list (tools/{eval,graph,runs}.py). eval_export reads summary.json file-direct so historical runs are queryable without a live backend.

Lifecycle is handled by BackendManager: it acquires a slot from the 8765-8769 flock pool, spawns + health-checks the backend, and captures the cleanup handle โ€” so each Claude Code conversation gets its own backend and the ยง5.1 / ยง5.3 friction is closed for the MCP path. What remains open: the tool set is a subset (no graph mutation / component introspection yet), HTTP/SSE transport for remote harnesses is not wired, and ยง5.2 (auth/sandbox) and ยง5.5 (iteration bookkeeping) are untouched. The point stands โ€” the HTTP surface is shaped well enough that the MCP layer on top is thin.

AgentCanvas docs