From eb3e9a72bf094a5fd6cff068353d2e49b9878ce2 Mon Sep 17 00:00:00 2001 From: Hamza Agar Date: Wed, 29 Jul 2026 01:22:23 +0300 Subject: [PATCH 1/2] docs(website): carry the new frame family into the pages that list frames The feature commit added the authoring guides but left the site's *inventory* pages describing the closed pre-0.3 wire: concepts, protocol overview, identity and persistence all enumerated PERSISTENT_FRAME_TYPES without the open rich message entry, the event-mapping tables had no `$mekik:"message"` row (nor mount/choose/message in the helper table), and the conformance fixture table was missing `rich-message`. A reader landing on any of them would have concluded the frame list was closed. Also drops the "five helpers" claim in the helpers page, which the same commit had already made false. Co-Authored-By: Claude Opus 5 (1M context) --- website/docs/authoring/helpers.md | 4 ++-- website/docs/concepts.md | 3 ++- website/docs/parity/conformance.md | 1 + website/docs/persistence.md | 2 +- website/docs/protocol/event-mapping.md | 4 ++++ website/docs/protocol/identity.md | 2 +- website/docs/protocol/overview.md | 2 ++ 7 files changed, 13 insertions(+), 5 deletions(-) diff --git a/website/docs/authoring/helpers.md b/website/docs/authoring/helpers.md index 59e9f7b..b6d02f2 100644 --- a/website/docs/authoring/helpers.md +++ b/website/docs/authoring/helpers.md @@ -1,12 +1,12 @@ --- sidebar_position: 1 title: Authoring helpers -description: The five node-authoring helpers — text, ui, event, tool, approve — that emit generative UI, tool traces, and human-in-the-loop pauses from inside an ilmek node, in TypeScript and .NET. +description: The node-authoring helpers — text, ui, mount, event, message, tool, approve, choose — that emit generative UI, rich messages, tool traces, and human-in-the-loop pauses from inside an ilmek node, in TypeScript and .NET. --- # Authoring helpers -Inside an ilmek node you shape the conversation with five helpers. They're the entire authoring surface — everything a turn can produce on the wire comes from one of them. Each takes ilmek's context (no ambient storage; ilmek already threads it through every node) and emits a `custom` event the [mapper](../protocol/event-mapping.md) turns into a frame. +Inside an ilmek node you shape the conversation with a handful of helpers. They're the entire authoring surface — everything a turn can produce on the wire comes from one of them. Each takes ilmek's context (no ambient storage; ilmek already threads it through every node) and emits a `custom` event the [mapper](../protocol/event-mapping.md) turns into a frame. diff --git a/website/docs/concepts.md b/website/docs/concepts.md index 215b040..faf91cf 100644 --- a/website/docs/concepts.md +++ b/website/docs/concepts.md @@ -138,12 +138,13 @@ The `MekikApp` it returns exposes three methods a transport calls — `connect(c A **frame** is a JSON object with a `type` discriminator. That's the entire wire. Frames split two ways. -**By direction.** Client→server frames are `hello`, `text`, `resume`, `genui_event`, `abort`. Server→client frames are `welcome`, `text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`, `run`, `error`. +**By direction.** Client→server frames are `hello`, `text`, `resume`, `genui_event`, `abort`. Server→client frames are `welcome`, `text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`, `run`, `error` — plus [rich message frames](./authoring/messages.md), whose `type` is a client message-renderer name rather than a protocol-owned one. **By persistence** — this is the important axis: ``` PERSISTENT_FRAME_TYPES = ["text", "tool_call", "genui", "interrupt", "interrupt_resolved"] + + any rich message frame (an open, renderer-named type) ``` - **Persistent** frames carry a per-conversation, strictly monotonic `seq`. They are appended to the transcript and are exactly what a reconnecting client replays. They *are* the durable record of the conversation. diff --git a/website/docs/parity/conformance.md b/website/docs/parity/conformance.md index 665e643..6a9e2be 100644 --- a/website/docs/parity/conformance.md +++ b/website/docs/parity/conformance.md @@ -59,6 +59,7 @@ The comparison is byte-for-byte over **canonical** JSON: UTF-8, object keys sort | `run-error` | `run_end{error}` → `⚠️` `text` + `run{error}` | | `run-aborted` | `run_end{aborted}` → `run{aborted}` only, no text | | `mixed-turn` | ui + tokens + tool + reply in one run (ordering + seq monotonicity) | +| `rich-message` | `mekik.message` customs → persistent [rich message frames](../authoring/messages.md); a caller-supplied id wins over the minted one; a reserved frame type is dropped | Each row is a claim about the [event→frame mapping](../protocol/event-mapping.md), frozen as JSON. diff --git a/website/docs/persistence.md b/website/docs/persistence.md index 4a2765c..b5108ce 100644 --- a/website/docs/persistence.md +++ b/website/docs/persistence.md @@ -47,7 +47,7 @@ The checkpointer is ilmek's contract, not mekik's — mekik only threads it into ## HistoryStore — the transcript -The `HistoryStore` holds the persistent frames (`text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`) in `seq` order. It is exactly what reconnect replays: the engine reads "every frame with `seq > watermark`" from it. Transient frames (`welcome`, `run`, `error`) are never written here — they're live-only. +The `HistoryStore` holds the persistent frames (`text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`, and any [rich message frame](./authoring/messages.md)) in `seq` order. It is exactly what reconnect replays: the engine reads "every frame with `seq > watermark`" from it. Transient frames (`welcome`, `run`, `error`) are never written here — they're live-only. ```ts interface HistoryStore { diff --git a/website/docs/protocol/event-mapping.md b/website/docs/protocol/event-mapping.md index 58f31c6..02bc77a 100644 --- a/website/docs/protocol/event-mapping.md +++ b/website/docs/protocol/event-mapping.md @@ -37,6 +37,7 @@ Most of these produce **nothing** in v1. The mapper cares about four: `run_start | `custom` | `isToken(payload)` (ilmek `{type:"token",text,meta?}`) | a `genui` text chunk `{type:"text", content: payload.text, id: nextChunkId}`, `done:false` | | `custom` | `payload.$mekik == "genui"` | a `genui` frame with `chunk = payload.chunk`; assign `chunk.id = nextChunkId` if absent; `done:false` | | `custom` | `payload.$mekik == "tool"` | a `tool_call` frame `{data: payload.call}` (upsert by `call.id`) | +| `custom` | `payload.$mekik == "message"` | a [rich message frame](../authoring/messages.md) `{type: payload.messageType, id: payload.id ?? mint(), from:"bot", data: payload.data, timestamp}` — **dropped** if `messageType` is a reserved protocol frame type (except `"text"`) | | `custom` | otherwise | nothing (reserved for the `onCustom` extension hook) | | `node_*`, `step_start`, `state`, `checkpoint` | — | nothing in v1 | | `interrupt` | for each `p` in `pending` | one `interrupt` frame `{id: p.id, data: unwrapInterrupt(p.payload)}` | @@ -56,8 +57,11 @@ You never write those `custom` payloads by hand. The [authoring helpers](../auth | `mekik.text(ctx, content)` | ilmek token event | `genui` text chunk | | `mekik.ui(ctx, component, props)` | `{$mekik:"genui", chunk:{type:"ui",…}}` | `genui` ui chunk | | `mekik.event(ctx, name, payload)` | `{$mekik:"genui", chunk:{type:"event",…}}` | `genui` event chunk | +| `mekik.mount(ctx, component, props)` | `{$mekik:"genui", chunk:{type:"ui",…, id}}` (same id on every `update`) | `genui` ui chunk, updated in place | | `mekik.tool(ctx, name, params, fn)` | `{$mekik:"tool", call:{…}}` (running → completed/error) | `tool_call` upsert | | `mekik.approve(ctx, payload, opts)` | `ctx.interrupt` with `$mekik:{ui,actions}` | `interrupt` frame | +| `mekik.choose(ctx, payload, options)` | `ctx.interrupt` with `$mekik:{actions}` built from the options | `interrupt` frame | +| `mekik.message(ctx, type, data)` | `{$mekik:"message", messageType, data, id?}` | rich message frame | The `$mekik` key is the reserved namespace the mapper keys on. It's an implementation detail of the helpers — but knowing it exists explains the fixtures. diff --git a/website/docs/protocol/identity.md b/website/docs/protocol/identity.md index e582dbc..eb257b4 100644 --- a/website/docs/protocol/identity.md +++ b/website/docs/protocol/identity.md @@ -41,7 +41,7 @@ This is the subtle rule ports get wrong. A stale watermark against a fresh conve ## The watermark and replay -Every persistent frame (`text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`) carries a per-conversation, strictly monotonic, gap-free `seq`. The **watermark** is the highest `seq` a client has durably received. +Every persistent frame (`text`, `tool_call`, `genui`, `interrupt`, `interrupt_resolved`, and any [rich message frame](../authoring/messages.md)) carries a per-conversation, strictly monotonic, gap-free `seq`. The **watermark** is the highest `seq` a client has durably received. On (re)connect: diff --git a/website/docs/protocol/overview.md b/website/docs/protocol/overview.md index 31e6928..c7f1a19 100644 --- a/website/docs/protocol/overview.md +++ b/website/docs/protocol/overview.md @@ -19,6 +19,7 @@ Frames are JSON objects with a `type` discriminator, exchanged over WebSocket, o ``` client → server: hello · text · resume · genui_event · abort server → client: welcome · text · tool_call · genui · interrupt · interrupt_resolved · run · error + · — any client message-renderer name (image, card, carousel, …) ``` **Persistence.** This is the axis that makes reconnect work: @@ -26,6 +27,7 @@ server → client: welcome · text · tool_call · genui · interrupt · interr ``` PERSISTENT (carry a per-conversation seq, stored, replayed): text · tool_call · genui · interrupt · interrupt_resolved + · — the one open entry: any renderer-named type TRANSIENT (live-only, never stored, never replayed): welcome · run · error From 9124e993376b2f9106ff578d5cc9cc9550a52dd9 Mon Sep 17 00:00:00 2001 From: Hamza Agar Date: Wed, 29 Jul 2026 01:28:01 +0300 Subject: [PATCH 2/2] docs(readme): link the published packages as badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README's only badge was CI, so a reader had no way to see — or click through to — what's actually published. Four badges, one per package a consumer installs directly: @mekik/core and @mekik/ws on npm, Mekik.Core and Mekik.AspNetCore on NuGet. Each renders the live version, so they double as the answer to "what's the current release?". @mekik/langchain and @mekik/redis are left out deliberately: they're optional integrations, listed in the layout section, and six badges would bury the CI one. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 91ac6f5..e3134c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # mekik [![CI](https://github.com/AimTune/mekik/actions/workflows/ci.yml/badge.svg)](https://github.com/AimTune/mekik/actions/workflows/ci.yml) +[![npm @mekik/core](https://img.shields.io/npm/v/%40mekik%2Fcore?logo=npm&label=%40mekik%2Fcore)](https://www.npmjs.com/package/@mekik/core) +[![npm @mekik/ws](https://img.shields.io/npm/v/%40mekik%2Fws?logo=npm&label=%40mekik%2Fws)](https://www.npmjs.com/package/@mekik/ws) +[![NuGet Mekik.Core](https://img.shields.io/nuget/v/Mekik.Core?logo=nuget&label=Mekik.Core)](https://www.nuget.org/packages/Mekik.Core) +[![NuGet Mekik.AspNetCore](https://img.shields.io/nuget/v/Mekik.AspNetCore?logo=nuget&label=Mekik.AspNetCore)](https://www.nuget.org/packages/Mekik.AspNetCore) **The realtime serving layer for [ilmek](https://www.npmjs.com/package/@ilmek/core) graphs.** mekik turns a running ilmek graph into a live conversation: a client (the [chativa](https://github.com/AimTune/chativa)