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) 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