TypeScript over HTTP
There is no TypeScript client. The gateway’s API is JSON over HTTP, so
fetch is enough. This file reads a record, waits where your model call
would go, and appends one entry naming the version it read. Node 24 and
later strip the types and run the file directly. This run used 26.7.0.
const BASE = "http://127.0.0.1:8080";
const TOKEN = "rem-token";
const RECORD = "incidents/inc-21";
type Field = { value: unknown; version: number };
async function call(path: string, init: RequestInit = {}) {
const res = await fetch(`${BASE}/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
});
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error), body);
return body;
}
for (;;) {
const rec = await call(`/records/${RECORD}`);
const plan = rec.fields.planned_action as Field;
console.log(`read planned_action v${plan.version}: ${JSON.stringify(plan.value)}`);
await new Promise((r) => setTimeout(r, 3000)); // your model call goes here
try {
await call(`/records/${RECORD}/fields/actions_taken/entries`, {
method: "POST",
body: JSON.stringify({
entry: `executed: ${plan.value}`,
evidence: "kubectl output",
based_on: [{ field: "planned_action", version: plan.version }],
}),
});
console.log("write accepted");
break;
} catch (e: any) {
if (e.error !== "STALE_PREMISE") throw e;
for (const m of e.moved) {
console.log(`rejected: ${m.field} moved v${m.your_version} -> v${m.current_version} by ${m.current_author}`);
}
}
}Run it with the gateway from Install and
another agent writing planned_action. The diagnoser moved the field
while this file was deciding:
read planned_action v1: "restart payments-api pods"
rejected: planned_action moved v1 -> v2 by apikey:diagnoser
read planned_action v2: "roll back api deployment"
write acceptedThree things carry that behaviour:
| In the code | What it does |
|---|---|
plan.version from the get | The version your decision is based on. Every read returns one per field. |
based_on on the append | Declares that premise. The gateway rejects the write if the field has moved. |
e.moved on the 409 | Names each premise that moved, with the version and author that replaced it. |
The Python and Go clients have a Session that remembers versions and
fills in based_on for you. In TypeScript you hold them yourself, as
plan.version does here. Keep them out of any framework state object
that outlives one cycle, because another agent’s write makes them wrong.
Handling rejections lists the other error bodies. The HTTP API has every route and field.