Drive Wayland Core from a Node Host (JSON Stream)
Wayland Core can run as a subprocess that speaks a line-based JSON protocol over stdin and stdout. This lets a host program, such as a Node app, drive the agent: send messages, receive streamed text, approve tool calls, and inject MCP servers. This tutorial builds a minimal Node host. For the full event and command catalog, see JSON Stream Protocol.
1. Install the engine as a dependency
Section titled “1. Install the engine as a dependency”The npm package resolves the platform-correct binary through optional dependencies and exposes its path. Install it in your project:
npm install @ferroxlabs/wayland-coreThe launcher package exports binaryPath(), which returns the absolute path to the binary npm installed for your platform. A host should spawn that path directly rather than going through the npx shim.
2. Spawn the process
Section titled “2. Spawn the process”Spawn the binary with --json-stream and the provider flags. Pass the API key through the environment.
const { spawn } = require("node:child_process");const { binaryPath } = require("@ferroxlabs/wayland-core");
const child = spawn( binaryPath(), ["--json-stream", "--provider", "anthropic", "--model", "claude-sonnet-4-20250514"], { stdio: ["pipe", "pipe", "inherit"], env: { ...process.env, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY } });
function send(obj) { child.stdin.write(JSON.stringify(obj) + "\n");}Note that stderr is inherited: it carries diagnostic logs and is not part of the protocol. Only stdout is JSON.
3. Read events line by line
Section titled “3. Read events line by line”Every line on stdout is one JSON object with a type field. Buffer stdout and split on newlines.
let buf = "";child.stdout.on("data", (chunk) => { buf += chunk; let nl; while ((nl = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, nl); buf = buf.slice(nl + 1); if (line.trim()) handle(JSON.parse(line)); }});The first event you receive is ready. It carries the protocol version, an optional session_id, and a capabilities object describing what this session supports, such as tool_approval and mcp. The client must wait for ready before sending any message.
4. Inject an MCP server (optional)
Section titled “4. Inject an MCP server (optional)”Between ready and your first message, you may inject MCP servers. This window closes once the first message is sent.
send({ type: "add_mcp_server", name: "my-tools", transport: "stdio", command: "node", args: ["bridge.js"],});The engine connects the server and emits mcp_ready with the list of registered tool names. Any add_mcp_server sent after the first message is rejected with an error event (code engine_error).
5. Send a message and stream the reply
Section titled “5. Send a message and stream the reply”Send a message with a client-generated msg_id:
send({ type: "message", msg_id: "m1", content: "Read package.json and summarize the dependencies" });The engine replies with a stream_start, a series of text_delta events you concatenate, and a stream_end carrying token usage. Render text_delta.text as it arrives.
6. Handle tool approvals
Section titled “6. Handle tool approvals”When the agent wants a tool that needs approval, it emits tool_request and pauses. You must respond with tool_approve or tool_deny keyed by the same call_id.
function handle(ev) { if (ev.type === "tool_request") { // Approve read-only info tools, ask a human for the rest. if (ev.tool.category === "info") { send({ type: "tool_approve", call_id: ev.call_id, scope: "once" }); } else { send({ type: "tool_deny", call_id: ev.call_id, reason: "needs human review" }); } }}The scope field is once for this call only, or always to add the tool’s category to the session allow-list. On approval the engine emits tool_running then tool_result. On denial it emits tool_cancelled, feeds the reason back to the model, and continues. If you would rather not gate at all in a trusted host, send { "type": "set_mode", "mode": "yolo" } to auto-approve everything for the session.
7. Shut down cleanly
Section titled “7. Shut down cleanly”To end the session, close stdin. The engine sees EOF, cleans up, and exits. You can also send a stop command to abort an in-flight turn without ending the process.
child.stdin.end();What you built
Section titled “What you built”You spawned the engine from Node, read its streamed events, optionally injected an MCP server, sent a message, gated tool calls by category, and shut down on EOF. From here, persist session_id to resume with --resume, and consult the protocol reference for the full event set including errors, thinking, and runtime config changes.