Nodes
This guide is the implementation reference for Agent Node: a self-hosted
agent-runtimes server that registers to Datalayer Runtimes and serves as
a node-local execution endpoint for AI agent workloads.
It is the canonical developer documentation. End-user documentation lives in
the central UI under Docs → Agent Nodes.
Documentation Map
- End-user overview: /docs/agent-nodes
- End-user localhost workflow: /docs/agent-nodes-localhost
- End-user AWS workflow: /docs/agent-nodes-aws
- CloudFormation definitions source: github.com/datalayer/agent-runtimes/tree/main/aws
If you are operating a Dockerized node, the local entrypoint is typically
http://localhost:8765 and resolves to the Agent Node UI
(/html/agent-node.html) in node mode.
Scope and Goals
- End-to-end Agent Node lifecycle: register, heartbeat, health, configure mode, list, inspect, and route chat.
- Inference routing control per Agent Node runtime:
inferenceProvider = local | datalayer. - Keep configuration source-of-truth local on each node.
- Mode taxonomy:
private | shared | sleep. - Central visibility: own nodes (all modes) + others only in
sharedmode. - Datalayer Runtimes (not operator) is the control plane and tunnel hub.
- Ship a dockerized runtime and a simple local development workflow.
Architecture Decisions
- Datalayer Runtimes is the system of record for Agent Nodes;
datalayer-operatoris no longer involved in Agent Node lifecycle. - The central service mints node identity (ULID) on first register. The local node persists whatever id the service returns and reuses it on subsequent calls.
- A node-to-runtimes tunnel is required: UI ↔ Node chat traffic transits through Datalayer Runtimes; nodes never accept inbound traffic from the UI.
- Visibility model in the central UI:
- show my nodes (all modes) with live status,
- show other users' Shared Mode nodes only,
- never expose other users'
privateorsleepnodes.
- Agent Node UI is node-local administration; the central UI is read-only for configuration and operational visibility.
- One docker image launches
agent-runtimes+ Agent Node UI supporting all three modes.
System Architecture
An Agent Node never exposes an inbound port to the internet. Instead it dials out to Datalayer Runtimes and holds a persistent WebSocket tunnel. All control (register/heartbeat/health) and data (chat) traffic flows through that outbound connection, brokered by the central service.
Datalayer Runtimes (control plane + hub)
┌──────────────────────────────────────┐
Central UI (SaaS, │ registry (_REGISTRY) │
VS Code, JupyterLab) │ tunnels (_TUNNELS) │
│ │ │
│ 1. POST .../{node_id}/ag-ui (RunAgentInput) │
▼ │ │
┌─────────┐ AG-UI │ ┌────────────┐ queue ┌───────┐ │ ┌──────────────┐
│ Chat │──SSE──────┼─▶│ ag-ui proxy│──────────▶ │tunnel │ │ WS │ Agent Node │
│ view │◀──────────┼──│ endpoint │◀────────── │ state │◀┼════════│ (agent- │
└─────────┘ events │ └────────────┘ reverse └───────┘ │ outbound│ runtimes) │
│ events │ dial └──────┬───────┘
└──────────────────────────────────────┘ │
active agent
(ephemeral nb/doc)
Registration & health (control plane)
The node's background sync task dials the central service and keeps the registry fresh:
POST /register(nonode_idon first call) → service mints a ULID and returns it; node persists it asnode_uid.POST /heartbeatandPOST /healthon a cadence, plus an immediatehealthon any mode change.- Missing beacons → node marked stale, then evicted from
_REGISTRYafterDATALAYER_AGENT_NODE_EVICT_SECONDS(its_TUNNELSentry is discarded too).
The tunnel (data plane)
The tunnel is a per-node, owner-scoped request/response channel held in
_TUNNELS as a TunnelState:
queue_to_node— messages waiting to be delivered to the node.reverse_events— a bounded deque of events coming back from the node.connected/last_seen_at— liveness for the WebSocket.
The node connects GET .../tunnel/ws and pumps both directions:
- UI/proxy enqueues
{type: "chat.request", payload, request_id}→ appended toqueue_to_nodeand recorded as aui_to_nodeevent. - The node's WS loop receives
{"type": "ui_message", request_id, payload}, runs the request against its active agent (_resolve_default_agent_id) using an ephemeral notebook/document, and replies with{"type": "chat.response", request_id, payload: {text, usage, metadata}}(orchat.error). - The server WS handler appends that reply as a
node_to_uireverse event carrying the samerequest_id.
Correlation is by request_id, so concurrent chats over one tunnel do not
cross wires.
The AG-UI proxy (protocol bridge)
The tunnel is request/response, but chat clients speak streaming AG-UI SSE.
POST /api/runtimes/v1/agent-nodes/{node_id}/ag-ui bridges the two so the
central Chat view can point at a normal endpoint URL — no client awareness of
the tunnel required:
- Authenticate the caller and resolve a visible node (
404if none,409if the node is insleepmode). - Parse the
RunAgentInputbody (threadId,runId,messages), extract the prompt, and enqueue achat.requeston the tunnel with a freshrequest_id. - Return a
StreamingResponse(text/event-stream) that immediately emitsRUN_STARTED, then_await_tunnel_response()pollsreverse_eventsfor a matchingnode_to_uireply. - On success, wrap the returned text as one
TEXT_MESSAGE_START/TEXT_MESSAGE_CONTENT(delta = text) /TEXT_MESSAGE_END, thenRUN_FINISHED. Onchat.erroror aDATALAYER_AGENT_NODE_AGUI_TIMEOUT_SECONDStimeout, emitRUN_ERROR.
Localhost deployments can also serve node-local chat directly from the node UI. AWS deployments are commonly configured as SaaS-only chat: prompts enter through the central AG-UI proxy, while execution still happens on the node.
AWS account discovery allowlist
For AWS nodes that do not use node-local interactive sign-in, the node sends
AWS identity metadata (aws_account_id, aws_region, aws_identity_arn) in
its registration payload (detected via boto3 STS GetCallerIdentity).
datalayer-runtimes keeps an in-memory allowlist per requester. Calling
POST /api/runtimes/v1/agent-nodes/aws/discover with a 12-digit account id:
- allowlists that AWS account id for the caller, and
- returns currently available nodes registered with that account id.
Those nodes then become visible to that caller in the Agent Nodes list/chat flows even when not shared.
Modes
| Mode | Purpose |
|---|---|
private | Owner-only execution under a selected billing entity. |
shared | Discoverable / usable by others per the sharing policy. |
sleep | Registered and visible but does not accept execution. |
Inference Provider
Agent Node supports two inference providers:
| Provider | Behavior |
|---|---|
local | Uses local model/provider resolution in agent-runtimes. |
datalayer | Routes LLM calls through datalayer-ai-inference (/api/ai-inference/v1/*). |
Effective Default
- In Agent Node mode, the effective default is
datalayer. - Outside Agent Node mode, the default remains
local. AGENT_RUNTIMES_INFERENCE_PROVIDER_OVERRIDEcan pin either value.
Runtime Configuration API
The node-local configuration API exposes inference controls:
GET /api/v1/configure/inference/providerPUT /api/v1/configure/inference/providerGET /api/v1/configure/inference/models
/inference/models is provider-aware:
- For
local, returns an empty model list. - For
datalayer, proxiesdatalayer-ai-inference /models. - If upstream returns no models (or is unavailable), Agent Node applies a
Bedrock Anthropic fallback list derived from
agentspecs(with env fallback), so the UI can still present usable choices. - For
datalayer, the payload may also includebedrock_anthropic_model_specsmetadata used by the UI to preselect the active default model.
Mode is part of every heartbeat and every health report. Changing the mode in the local UI synchronously triggers an extra health report so the central registry reflects the new state immediately.
Canonical Models
AgentNodeConfiguration
{
"mode": "private", // private | shared | sleep
"node_uid": "01JABCDE...", // ULID assigned by central service
"active_agent_id": "default", // agent selected in the gallery to serve chat
"billing_entity_uid": "...",
"billing_entity_type": "...",
"billing_entity_handle": "...",
"sharing": { /* sharing policy for shared mode */ }
}
AgentNodeHealth
{
"mode": "private",
"hostname": "node-host",
"platform": "Linux",
"platform_release": "6.5.0-...",
"python_version": "3.13.0",
"agent_runtimes_version": "dev",
"cpu_count": 16,
"cpu_percent": 12.3,
"memory_total_mb": 64000,
"memory_available_mb": 41280,
"load_average": [0.32, 0.45, 0.51],
"uptime_seconds": 1234.5,
"reported_at": "...",
"reason": "periodic" // periodic | mode_change | startup
}
AgentNodeRecord (central)
Adds owner identity, status, timestamps, capabilities, plus the latest
health snapshot and last_health_at.
Identity (node_uid)
- The central
datalayer-runtimesservice is the sole authority for minting Agent Node identifiers. - On the first
POST /api/runtimes/v1/agent-nodes/registerfrom a node, the service mints a ULID and returns it asagent_node.node_id. - The local node persists that id as
configuration.node_uidin~/.datalayer/agent-node.json(override withAGENT_NODE_STATE_PATH) and reuses it for all subsequent/register,/heartbeat, and/healthcalls. AGENT_NODE_IDremains an operator escape hatch for externally pinned ids. When set, no service-side minting occurs and the value is used verbatim.
Synchronization Loop
The background sync task in agent_runtimes/nodes/agent_node_sync.py:
- Registers with the central service. On first call, omits
node_id; captures the assigned ULID from the response and persists it. - Sends a
heartbeatwith the current configuration. - Sends a
healthsnapshot:- on startup (
reason="startup"), - every
AGENT_NODE_HEALTH_SECONDS(default60s,reason="periodic"), - immediately on any local mode change (
reason="mode_change").
- on startup (
- Waits up to
AGENT_NODE_HEARTBEAT_SECONDS(default20s) before the next tick, waking early on stop or mode-change.
Mode-change triggering uses register_mode_change_callback() exported by
routes/agent_node.py. The callback fires inside set_agent_node_configuration
when the new mode differs from the persisted one.
Endpoints
Local node — agent-runtimes
GET /api/v1/agent-node/configuration— read persisted configuration includingnode_uid.POST /api/v1/agent-node/configuration— replace configuration. Thenode_uidis owned by the central service and is never overwritten by this endpoint; onlyset_agent_node_uid()updates it.GET /api/v1/agent-node/active-agent— read the node's active agent id.POST /api/v1/agent-node/active-agent— set the active agent id (the agent that chat sessions target). Persisted in the node configuration.GET /api/v1/configure/inference/provider— read effective runtime inference provider (local | datalayer).PUT /api/v1/configure/inference/provider— update runtime inference provider for subsequent launches/sessions.GET /api/v1/configure/inference/models— list available models for the active provider (Bedrock Anthropic focused fordatalayer).
Central — datalayer-runtimes
POST /api/runtimes/v1/agent-nodes/register—node_idoptional; ULID minted viadatalayer_common.utils.new_ulidwhen missing.POST /api/runtimes/v1/agent-nodes/heartbeat—node_id+ currentconfiguration.POST /api/runtimes/v1/agent-nodes/configuration— update configuration.POST /api/runtimes/v1/agent-nodes/health— periodic / on-demand health snapshot. Also refresheslast_seen_atand, ifhealth.modediffers from the registered configuration, reconciles the registered mode.GET /api/runtimes/v1/agent-nodes— list visible nodes (own + others shared).GET /api/runtimes/v1/agent-nodes/{node_id}— fetch one node.POST /api/runtimes/v1/agent-nodes/{node_id}/ag-ui— AG-UI compatible chat proxy. Accepts a standardRunAgentInputbody, forwards the prompt over the tunnel to the node's active agent, and streams the reply back as AG-UI SSE events (RUN_STARTED,TEXT_MESSAGE_START/CONTENT/END,RUN_FINISHED;RUN_ERRORon timeout or node error). Returns409when the node is insleepmode. Timeout is controlled byDATALAYER_AGENT_NODE_AGUI_TIMEOUT_SECONDS(default180). This is what the central UI's node Chat view points at, so the node never needs an inbound public endpoint.- Tunnel endpoints:
GET .../tunnel/ws,POST .../{node_id}/tunnel/messages,GET .../{node_id}/tunnel/events,GET .../{node_id}/tunnel/status.
Registry Eviction
The central registry tracks last_seen_at per node from register / heartbeat
/ health calls. Nodes that stop reporting are first surfaced as stale and
then evicted after DATALAYER_AGENT_NODE_EVICT_SECONDS (default 300) so the UI
only lists reachable nodes. An evicted node that restarts simply
re-registers with its persisted node_uid.
Environment Variables
| Variable | Purpose |
|---|---|
DATALAYER_RUNTIMES_URL | Base URL of central runtimes service. |
DATALAYER_API_KEY | Bearer token for register/heartbeat/health. |
AGENT_NODE_ID | Optional externally pinned node id. |
AGENT_NODE_NAME | Optional display name (defaults to hostname). |
AGENT_NODE_MODE | Initial mode (only on first start before any persistence). |
AGENT_NODE_STATE_PATH | Override for the persisted configuration JSON. |
AGENT_NODE_HEARTBEAT_SECONDS | Heartbeat cadence (default 20). |
AGENT_NODE_HEALTH_SECONDS | Health cadence (default 60). |
DATALAYER_AGENT_NODE_EVICT_SECONDS | Central grace period before a silent node is evicted (default 300). |
AGENT_NODE_AGENT_ID | Fallback active agent id when none is set in configuration. |
DATALAYER_AGENT_NODE_AGUI_TIMEOUT_SECONDS | Central AG-UI proxy wait for a node reply (default 180). |
AGENT_RUNTIMES_VERSION | Embedded in heartbeat / health payloads. |
AGENT_RUNTIMES_INFERENCE_PROVIDER_OVERRIDE | Runtime override for local / datalayer. |
DATALAYER_AI_INFERENCE_URL | Base URL for datalayer-ai-inference (default local :4450). |
DATALAYER_INFERENCE_PROVIDER | ai-inference backend provider (e.g. bedrock, azure). |
DATALAYER_BEDROCK_* | Bedrock credentials/region/model inputs used by ai-inference. |
UI Flow (Node-Local)
The src/AgentNode.tsx app exposes these steps:
- Authentication — sign in (token or API key). Configuration / Agents / Chat header entries are hidden until authenticated.
- Configuration — pick mode, billing entity, and sharing policy.
Also pick Inference provider (
localordatalayer). Whendatalayeris selected, the UI shows available Bedrock Anthropic models returned by/api/v1/configure/inference/modelsas a read-only selector. The active option is preselected from model metadata/defaults, and model switching is intentionally locked in this view. - Agents — an agent gallery built from the library
(
GET /api/v1/agents/library) and running agents (GET /api/v1/agents). Launching a card starts (or reuses) the agent and callsPOST /api/v1/agent-node/active-agentto mark it active. The active agent is what chat targets; an Active badge shows the current selection. - Chat — node-local AG-UI chat against the active agent, backed by an
ephemeral notebook and document. Only enabled when
configuration.mode === 'private'.
In private mode the flow is Configuration → Agents → Chat. If mode is
changed away from private while in the Agents or Chat view, the user is
returned to the configuration view.
Security and Authorization
- Runtimes is the single public control plane for Agent Nodes.
- The tunnel is authenticated and bound to the owner identity.
- UI requests are validated by user identity and node access policy.
- Shared-mode access follows the explicit sharing policy.
Development Workflow
# Local UI + Python server
make agent-node
# Local UI + Python against a local plane stack
make agent-node-local
# Run a real Agent Node against `plane local` services: registers to the
# local runtimes service with heartbeat/health, then is evicted on stop.
make agent-node-dist
# Dockerized runtime
make agent-nodes-docker-build DOCKER_TAG=dev
docker run --rm -p 8765:8765 datalayer/agent-nodes:dev
For local proxy development, make agent-node-local exports both
DATALAYER_AI_INFERENCE_URL and VITE_DATALAYER_AI_INFERENCE_URL to the
local ai-inference service (http://localhost:4450 by default).
plane local also starts the local ai-inference service and waits for
/api/ai-inference/v1/health readiness.
Risks and Mitigations
- Tunnel instability — reconnect with backoff, heartbeats, session expiry.
- Visibility regressions — explicit policy tests for own/shared-only queries.
- Node-local vs central drift — local configuration and central registry can diverge transiently during network disruption; mitigated by periodic heartbeat/health sync plus immediate mode-change health reports.
- Registry restart loss — v1 in-memory registry is documented as expected; persistence is a follow-up.
Acceptance Criteria
- Node can run in
private,shared, andsleepmodes. - Node registers and heartbeats directly against the runtimes-owned registry; ULID is minted server-side on first register.
- Health is reported on startup, on a fixed cadence, and on every mode change.
- Tunnel routes chat traffic both ways via runtimes.
- UI Agent Nodes page shows my nodes (all modes) + other users' shared nodes, including node id, mode, status, host, and last-seen.
- UI supports navigation from a node row/detail to chat via the selected node.
- Docker image launches node UI + server with all three modes.
make agent-nodesupports easy local UI + Python development.