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