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.
Every package is versioned together (currently 0.9.0).
| npm | NuGet | what it is |
|---|---|---|
@mekik/core |
Mekik.Core |
protocol, mapper, engine, authoring helpers, stores, auth, MCP/A2A servers |
@mekik/ws |
Mekik.AspNetCore |
the WebSocket transport (serveWs / MapMekik); in .NET also MapMekikMcp and MapMekikA2a |
@mekik/mcp |
— (Mekik.AspNetCore) |
the graph as MCP tools over Streamable HTTP (serveMcp) |
@mekik/a2a |
— (Mekik.AspNetCore) |
the graph as an A2A agent: Agent Card + JSON-RPC over HTTP (serveA2a) |
@mekik/langchain |
Mekik.Agents |
wrap an agent's tools: traces, approvals, exactly-once, skills (LangChain / Microsoft.Extensions.AI) |
| — | Mekik.SemanticKernel |
one function filter for Semantic Kernel agents and planners |
@mekik/redis |
Mekik.Redis |
Redis turn lock + Pub/Sub backplane, for a fleet |
@mekik/converger |
Mekik.Converger |
run behind the Converger hub: one outbound multiplexed WebSocket, with the external-transcript profile (connectConverger / ConvergerTransport.Connect) |
pnpm add @mekik/core @mekik/ws @ilmek/core@^0.1.1
dotnet add package Mekik.Core && dotnet add package Mekik.AspNetCoreimport { 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
CLIENT-TOOLS.md # the frontend's own tools, callable by the graph (§11)
SKILLS.md # Agent Skills: the catalog, progressive disclosure, the skill frame (§12)
MCP.md # MCP both ways: consuming servers, serving the graph as tools (§13)
A2A.md # the graph as an Agent2Agent peer: card, tasks, input-required (§14)
SCALING.md # running a fleet: turn lock, backplane, re-homing
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)
packages/mcp/ # @mekik/mcp — the graph as MCP tools over Streamable HTTP (§13)
packages/a2a/ # @mekik/a2a — the graph as an A2A agent: card + JSON-RPC over HTTP (§14)
packages/converger/ # @mekik/converger — run behind the Converger hub (§15)
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)
examples/client-tools.ts # the frontend's UI as callable tools (§11)
examples/banking/ # domain probe: skill-held money tools, two-approver transfers, a silent fraud-tool failure
examples/insurance/ # domain probe: form intake pause, tools held under a skill, typed rejection
examples/healthcare-triage/ # domain probe: redacted identifiers, client-declared skills, the page calendar as a client tool
examples/travel-booking/ # domain probe: skill-held booking, watermark replay across a reconnect, exactly-once cancel
examples/support-desk/ # domain probe: tag-scoped skills per route, MCP knowledge base, A2A hand-off
dotnet/
src/Mekik.Core/ # mirror of @mekik/core
src/Mekik.AspNetCore/ # MapMekik("/ws"), MapMekikMcp("/mcp"), MapMekikA2a("/a2a")
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)
src/Mekik.Converger/ # run behind the Converger hub (§15)
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.SqlAgent # mirror of the SQL agent (with --probe)
examples/Mekik.WeatherAgent # mirror of the weather agent (with --probe)
examples/Mekik.ServerComponents # mirror of the server-defined components demo
examples/Mekik.ClientTools # mirror of the client-tools demo (§11)
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 + every example self-test and probe
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 every example's self-test or probe
(the domain scenarios included); .NET builds, replays the same golden fixtures
through its own EventToFrames, comparing canonical JSON byte-for-byte, and runs
its offline example probes. 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, skill, genui, interrupt, interrupt_resolved, and
rich message frames) carry a per-conversation monotonic seq, are stored, and
replay on reconnect after a watermark. Transient frames (welcome, run,
error, skills, genui_components) 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. - Client tools invert the toolbox (§11, opt-in): the frontend declares its
own UI capabilities as tools in
hello.tools(with optionaltagsso one node sees a tool another does not), a node calls one withmekik.callClientTool, and the client's handler answers through the same durable interrupt machinery. - Skills (§12) give nodes a catalog of
SKILL.mdfolders (read by@ilmek/skills/Ilmek.Skills), disclosed to the model progressively:mekik.skillsPromptlists them,mekik.loadSkillhands back one skill's instructions and emits a persistentskillframe, and the catalog is announced to the client in a hash-versionedskillsframe. A catalog entry can own tools (SkillEntry.tools/SkillEntry<AIFunction>.Tools), which the agent loop offers only after the model has loaded that skill successfully. A frontend may declare its own skills behind an opt-in policy. - MCP (§13) runs both ways: an MCP server's tools join an agent's toolbox
with the same trace/exactly-once/approval treatment as server tools
(
withMcpTools,McpFunctions.Wrap, over@ilmek/mcp/Ilmek.Mcp), andMekikMcpServerexposes the graph itself as two MCP tools — a turn, and a resume for its human-in-the-loop pauses — over Streamable HTTP. - A2A (§14) makes the graph a peer agent:
MekikA2aServerserves an Agent Card andmessage/send/tasks/get/tasks/cancel, one turn per task, with a paused run as aninput-requiredtask the calling agent answers by replying on it. ilmek's@ilmek/a2a/Ilmek.A2Ais the calling side.
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,
docs/GENUI.md for the two rendering paths (typed GenUI
components and rich messages), docs/CLIENT-TOOLS.md
for client-declared tools, docs/SKILLS.md for skills,
docs/MCP.md for MCP in both directions, and
docs/A2A.md for the graph as an A2A peer.
Transports other than WebSocket for the chat wire (MCP and A2A serving are
separate doors, not transports of mekik/1); durable (Redis/Postgres) history
and conversation stores (the ports exist, only the in-memory ones ship); a
debug stream mode; subgraph ns surfacing; Go/Python ports. Horizontal scale
is no longer a non-goal: @mekik/redis / Mekik.Redis ship a Redis turn lock
and Pub/Sub backplane (see docs/SCALING.md).
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.
Report vulnerabilities privately via GitHub security advisories
or the contact in SECURITY.md. The repository is enrolled in
Anthropic's OSS Scanner; the build it
audits is .oss-scanner/Dockerfile and the threat
model it reads is .oss-scanner/threat_model.md.
MIT. See LICENSE.