Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions website/docs/authoring/helpers.md
Original file line number Diff line number Diff line change
@@ -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.

<Tabs groupId="lang">
<TabItem value="ts" label="TypeScript">
Expand Down
3 changes: 2 additions & 1 deletion website/docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions website/docs/parity/conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion website/docs/persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions website/docs/protocol/event-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)}` |
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion website/docs/protocol/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 2 additions & 0 deletions website/docs/protocol/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ 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
· <rich message> — any client message-renderer name (image, card, carousel, …)
```

**Persistence.** This is the axis that makes reconnect work:

```
PERSISTENT (carry a per-conversation seq, stored, replayed):
text · tool_call · genui · interrupt · interrupt_resolved
· <rich message> — the one open entry: any renderer-named type

TRANSIENT (live-only, never stored, never replayed):
welcome · run · error
Expand Down
Loading