Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

53 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mekik

CI npm @mekik/core npm @mekik/ws NuGet Mekik.Core NuGet Mekik.AspNetCore

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.

Why

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.

Quickstart (TypeScript)

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, …).

Layout

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.

Run it

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.Examples

With 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.LlmAgent

Each 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 -- --probe

The 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.

The protocol in one screen

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 genui frames carrying AIChunks (ui / text / event) — the same shape chativa already renders.
  • HITL is a first-class interrupt frame (optionally mounting a form via ui, with chip actions as fallback), answered by a resume frame keyed by the thread-scoped interrupt id, acknowledged by interrupt_resolved.
  • Tools emit tool_call running→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/.

Parity

The two implementations are held to the same wire two ways:

  1. Golden fixtures — recorded ilmek event streams plus the exact frames they must produce, in canonical JSON. Both eventToFrames implementations replay them and compare byte-for-byte. This is the closed, machine-checkable core.
  2. 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).

Non-goals (v1)

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.

License

MIT.

About

Realtime serving layer for ilmek graphs — WebSocket sessions, streaming generative UI, and durable human-in-the-loop over one wire protocol, in TypeScript and .NET.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages