The realtime serving layer for ilmek graphs. mekik turns a running ilmek graph into a live conversation: a client (the chativa widget) sends user turns and interrupt answers over a WebSocket; the server drives the graph and streams back text, generative UI, tool traces, and interactive human-in-the-loop pauses. One graph run == one conversational turn.
chativa ⇄ @chativa/connector-mekik ⇄ WebSocket ⇄ ConversationEngine ⇄ IlmekAdapter ⇄ ilmek graph
│ HistoryStore │ Checkpointer
│ ConversationStore │ (ilmek's own)
│ Authenticator
Two implementations — TypeScript (reference) and .NET (port) — speak one
byte-identical wire protocol, mekik/1, held to that promise by shared golden
fixtures. PROTOCOL.md is the normative spec.
mekik is the successor to a standalone 4-language connector whose protocol was
hand-mirrored "byte-for-byte" across TS, Go, .NET, and Python with no
cross-language test, whose LangGraph human-in-the-loop detection sniffed error
strings, and whose interrupts were a text+chips convention answered by "the next
user message." mekik/1 fixes those: it is ilmek-native (consumes ilmek's
typed event stream — no error-sniffing), interrupts are first-class frames
answered by thread-scoped id (concurrent pauses can't collapse), and parity is
two languages checked by golden fixtures, not four checked by hope.
import { graph, channel, START, END } from "@ilmek/core";
import { mekik } from "@mekik/core";
import { serveWs } from "@mekik/ws";
const g = graph("refund")
.channel("input", channel.lastWrite<string>(""))
.channel("reply", channel.lastWrite<string>(""))
.node("gate", async (s, ctx) => {
mekik.ui(ctx, "order-card", { id: s.input }); // stream GenUI
const ok = await mekik.approve<{ approved: boolean }>( // pause for a human
ctx,
{ title: `Refund ${s.input}?` },
{ ui: { component: "approval-form", props: { orderId: s.input } } },
);
return { reply: ok.approved ? "refunded" : "cancelled" };
})
.edge(START, "gate").edge("gate", END)
.compile();
const app = mekik({ graph: g, reply: (s) => s.reply as string });
serveWs(app, { port: 8800, path: "/ws" });The single mekik export is both the app factory (mekik({ graph })) and the
node-authoring helpers (mekik.ui, mekik.tool, mekik.approve, …).
mekik/
PROTOCOL.md # normative mekik/1 wire spec
conformance/
README.md # language-neutral scenario list
fixtures/*.json # golden event→frame fixtures (shared by both suites)
docs/
LANGUAGES.md # TS ↔ .NET naming parity
HITL.md # human-in-the-loop authoring guide
GENUI.md # rendering: typed components and rich messages
ts/
packages/core/ # @mekik/core — protocol, mapper, engine, helpers, stores, auth
packages/ws/ # @mekik/ws — WebSocket transport
packages/langchain/ # @mekik/langchain — wrap an agent's tools
packages/redis/ # @mekik/redis — Redis turn lock + Pub/Sub backplane (fleet)
examples/refund.ts # showcase: tool + GenUI + form approval + resume
examples/llm-agent.ts # the same desk, driven by a real Claude model
examples/sql-agent.ts # a model writing its own SQL over SQLite
examples/weather-agent.ts # chained network tools, fan-out, recovery
examples/concierge.ts # all three tool groups in one agent (single node)
examples/routed-desk.ts # the same desk as a real graph: router + per-domain nodes
examples/storefront.ts # rendering showcase: every typed component + message type
examples/server-components.ts # the backend defines the widget itself (§10)
dotnet/
src/Mekik.Core/ # mirror of @mekik/core
src/Mekik.AspNetCore/ # app.MapMekik("/ws", app)
src/Mekik.Agents/ # Microsoft.Extensions.AI function wrapping
src/Mekik.SemanticKernel/ # one filter covers SK agents and planners
src/Mekik.Redis/ # Redis turn lock + Pub/Sub backplane (fleet)
test/Mekik.Core.Tests/ # loads the SAME fixtures
examples/Mekik.Examples # mirror of the refund showcase
examples/Mekik.LlmAgent # mirror of the LLM-driven desk
examples/Mekik.ServerComponents # mirror of the server-defined components demo
mekik depends on ilmek as a published package — @ilmek/core
on npm and Ilmek.Core on NuGet — so
this repo stands alone; no sibling checkout is needed.
TypeScript (Node ≥ 22; developed on Node 26, uses the built-in .ts runner):
cd ts && pnpm install
pnpm check # build + tests + the refund self-test
node examples/refund.ts --serve # a real ws://localhost:8800 server (any path).NET (net9.0):
cd dotnet
dotnet test Mekik.slnx # conformance: same fixtures, canonical compare
dotnet run --project examples/Mekik.ExamplesWith a real model. The two llm-agent examples run the same refund desk with
nothing scripted: Claude reads the message and picks the tools itself, while mekik
surfaces each call, gates the refund behind a human, and journals both so a
resume never charges twice. They call the live API, so they need a key and are
kept out of the CI test path (CI still compiles them):
ANTHROPIC_API_KEY=sk-ant-… node ts/examples/llm-agent.ts # refunds
ANTHROPIC_API_KEY=sk-ant-… node ts/examples/sql-agent.ts # SQL over SQLite
ANTHROPIC_API_KEY=sk-ant-… node ts/examples/weather-agent.ts # a public HTTP API
ANTHROPIC_API_KEY=sk-ant-… node ts/examples/concierge.ts # all of the above, one node
ANTHROPIC_API_KEY=sk-ant-… node ts/examples/routed-desk.ts # the same, as a routed graph
ANTHROPIC_API_KEY=sk-ant-… dotnet run --project dotnet/examples/Mekik.LlmAgentEach of the newer examples also has a --probe mode that scripts only the
model's decisions (and, where relevant, the HTTP layer) and runs the identical
graph, tools and wire path. That is what CI runs, so the examples stay honest
without a key or a bill:
node ts/examples/concierge.ts --probe
dotnet run --project dotnet/examples/Mekik.SqlAgent -- --probeThe GenUI components these emit — data-table, weather-card, approval-form,
order-card — are registered in chativa's sandbox, so --serve renders end to
end against a real client.
One node or many? concierge.ts and routed-desk.ts are the same desk built
both ways, and the pair is the argument for ilmek being a graph rather than a
loop. The routed version classifies the turn in a router node, gives each domain
its own node with only its own tools, and makes the human-in-the-loop pause a
node of its own. That last split has a consequence you can see on the wire:
resuming replays one node, so the lookup that ran before the pause is neither
re-run nor re-emitted — where the single-node version re-sends its tool_call
frames for a query that never ran again. Both probes assert their own behaviour.
Both sides are green in CI: TypeScript builds, passes the golden
fixtures and behavioural scenarios, and runs the refund self-test; .NET builds and
replays the same golden fixtures through its own EventToFrames, comparing
canonical JSON byte-for-byte. That cross-language fixture run is what proves the
two implementations produce the identical wire.
Frames are JSON with a type discriminator, over WebSocket. Persistent frames
(text, tool_call, genui, interrupt, interrupt_resolved) carry a
per-conversation monotonic seq, are stored, and replay on reconnect after a
watermark. Transient frames (welcome, run, error) are live-only.
- GenUI streams as
genuiframes carryingAIChunks (ui/text/event) — the same shape chativa already renders. - HITL is a first-class
interruptframe (optionally mounting a form viaui, with chipactionsas fallback), answered by aresumeframe keyed by the thread-scoped interruptid, acknowledged byinterrupt_resolved. - Tools emit
tool_callrunning→completed/error traces; the side effect is journaled by ilmek so it runs exactly once across an interrupt/resume.
Full details in PROTOCOL.md; the exact event→frame mapping is
pinned by conformance/fixtures/.
The two implementations are held to the same wire two ways:
- Golden fixtures — recorded ilmek event streams plus the exact frames they
must produce, in canonical JSON. Both
eventToFramesimplementations replay them and compare byte-for-byte. This is the closed, machine-checkable core. - Scenario suites — the multi-frame, multi-run behaviours (handshake, replay, fan-out, resume routing, the turn lock, auth), written as ordinary tests in each language against the identical observable wire.
See docs/LANGUAGES.md for the naming map,
docs/HITL.md for the human-in-the-loop authoring rules, and
docs/GENUI.md for the two rendering paths (typed GenUI
components and rich messages).
Horizontal scale / distributed turn lock; transports other than WebSocket; durable
(Redis/Postgres) history stores (ports exist, in-memory ships); a debug stream
mode; Go/Python ports.
The client end is chativa's @chativa/connector-mekik, which already speaks
mekik/1: it renders interrupt frames as approval chips (or a mounted form),
answers them with a resume keyed by interrupt id, and re-renders open pauses
announced in welcome.pending after a reconnect.
MIT.