Skip to main content

Documentation Index

Fetch the complete documentation index at: https://internal.september.wtf/llms.txt

Use this file to discover all available pages before exploring further.

This page walks through your first Engine call from Node. We use the built-in fetch (Node 18+) and read the response body as a stream.

Set up

You need Node 18 or newer. No npm packages required — fetch and ReadableStream are built in. If you prefer TypeScript, the snippets below work as .ts files with tsx or ts-node.

A minimal client

Save this as engine-quickstart.mjs:
const ENGINE_URL = process.env.ENGINE_URL ?? "http://localhost:8000";
const ENGINE_KEY = process.env.ENGINE_KEY;

if (!ENGINE_KEY) {
  console.error("Set ENGINE_KEY in your environment.");
  process.exit(1);
}

async function execute(message, taskId) {
  const resp = await fetch(`${ENGINE_URL}/execute`, {
    method: "POST",
    headers: {
      "X-Engine-Key": ENGINE_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ message, task_id: taskId }),
  });

  if (!resp.ok) {
    throw new Error(`Engine returned ${resp.status}: ${await resp.text()}`);
  }

  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    // SSE messages are separated by a blank line.
    let messageEnd;
    while ((messageEnd = buffer.indexOf("\n\n")) !== -1) {
      const raw = buffer.slice(0, messageEnd);
      buffer = buffer.slice(messageEnd + 2);
      handleMessage(raw);
    }
  }
  process.stdout.write("\n");
}

function handleMessage(raw) {
  let eventType = null;
  let data = null;
  for (const line of raw.split("\n")) {
    if (line.startsWith("event:")) eventType = line.slice(6).trim();
    else if (line.startsWith("data:")) data = JSON.parse(line.slice(5).trim());
  }
  if (!data) return;
  if (data.text) process.stdout.write(data.text);
}

await execute("Say hello in one short sentence.", "demo-001");
Run it:
export ENGINE_URL=http://localhost:8000
export ENGINE_KEY=dev-engine-key

node engine-quickstart.mjs
You should see the agent’s response stream into your terminal.

Handling other event types

The example above only prints text. A real client should route on event type:
function handleMessage(raw) {
  let eventType = null;
  let data = null;
  for (const line of raw.split("\n")) {
    if (line.startsWith("event:")) eventType = line.slice(6).trim();
    else if (line.startsWith("data:")) data = JSON.parse(line.slice(5).trim());
  }
  if (!data) return;

  switch (eventType) {
    case "text_delta":
      process.stdout.write(data.text ?? "");
      break;
    case "tool_call":
      console.log(`\n[tool call: ${data.tool}]`);
      break;
    case "tool_result":
      console.log(`\n[tool result: ${(data.output ?? "").slice(0, 60)}...]`);
      break;
    case "hitl_request":
      console.log(`\n[engine asks: ${data.question}]`);
      break;
    case "thread_lifecycle":
      console.log(`\n[${data.phase}]`);
      break;
  }
}

TypeScript signatures

If you’re writing in TypeScript, here’s a minimal type for the events:
type EngineEvent =
  | { type: "text_delta";       text: string }
  | { type: "tool_call";         tool: string; input: unknown }
  | { type: "tool_result";       output: string }
  | { type: "hitl_request";      question: string; request_id: string }
  | { type: "thread_lifecycle";  phase: "started" | "completed" | "errored" };
The full event catalog is at Streaming events reference.

Where to go next