Coding-Agent Backend Surface
Programmatic API surface โ HTTP + filesystem + process model
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:
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.
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:
- 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 ports8765-8769and acquire the first one whose lockfile under~/.cache/agentcanvas-mcp/locks/{port}.lockisflock-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. - Spawn under
setsid -f. The resulting process tree hasconda runโuvicornโ manyauto_hostchildren, with eachauto_hostcallingos.setsid()to escape the parent's group (base_server.py:52). Withoutsetsid -fon the spawn, a singlekill -- -PGIDcleanup misses everyauto_host. With it, one PGID kill atomically reaps the conda wrapper and uvicorn; a separate per-auto_hostPGID kill reaps the rest. See/architect:experiment-httpstep 3 for the canonical sequence (the-mcpskill delegates this toBackendManagerinagentcanvas/mcp_server/). - Cleanup is mandatory and PGID-targeted. Never use
pkill -f uvicornorpkill -f auto_hostโ the user almost always has unrelateduvicorninstances 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.