@rohansguliani/mux-chat-core
v0.5.0
Published
Framework-free Mux chat engine: transcript merge rules, event reducers, snapshot reconciliation, optimistic delivery, and a single-session controller.
Downloads
1,117
Readme
@rohansguliani/mux-chat-core
Framework-free chat engine for the Mux runtime: embed a Mux chat in any app. The same engine runs Mux's own frontend, so hosts get the rules Mux's UI uses.
It owns the rules every chat surface must agree on:
| Module | Owns |
| --- | --- |
| order | Message/part identity and order, streaming merge rules, turn-diff revisions |
| text | UTF-8 positioned text deltas (idempotent, gap-detecting) |
| events | Event payload validation (message.part.delta, session.status, transcript resets) |
| store | The ChatStore write boundary; message.*, request, and status reducers; a copy-on-write createMemoryStore |
| snapshot | Reconciling an HTTP snapshot with live changes made while it was in flight |
| optimistic | Pending user messages until the server's copy arrives |
| text-gap | Repairing streamed text after a missed delta |
| delivery | At-most-once prompt delivery with lost-acknowledgement reconciliation |
| session | createChatSession: one session's stream, reconnect with Last-Event-ID, snapshots, history paging, pending prompts |
| runtime | A ChatTransport over the runtime's HTTP/SSE API for standalone hosts |
Hosts provide state and transport; the package never imports a UI framework.
- State: implement
ChatStoreover your store, or usecreateMemoryStore()and render fromstate()/subscribe(). Mux maps each operation to a Solid update (frontend/src/context/global-sync/chat-store.ts). - Transport: implement
ChatTransport(messages, optionalstate,stream,prompt), or usecreateRuntimeTransport.
Standalone use
import { createChatSession, createMemoryStore, createRuntimeClient, createRuntimeTransport, isSessionStreaming, sessionQuestions, sessionTurns, textSubmission } from "@rohansguliani/mux-chat-core"
const client = createRuntimeClient({ baseURL: "/runtime" }) // same-origin proxy holding the runtime password
// Resumes the stored session if it still exists in this workspace, else starts one.
const { workspaceID, session } = await client.openSession({ directory: "/srv/app-workspace", sessionID: localStorage.chat })
localStorage.chat = session.id
const store = createMemoryStore()
const chat = createChatSession({ store, transport: createRuntimeTransport(client, { workspaceID }) })
store.subscribe((state) => render(sessionTurns(state, session.id), sessionQuestions(state, session.id), isSessionStreaming(state, session.id)))
chat.subscribe((view) => renderStatus(view)) // connected, ready, error, pending…
chat.start(session.id)
await chat.send(textSubmission({ sessionID: session.id, text: "Why did NVDA move today?", agent: "assistant" }))Each snapshot also reloads the session's pending questions, permissions, and
status (GET /question, /permission, /session/status), so a page reload
never hides an open question. client.abort, answerQuestion,
rejectQuestion, replyPermission, and providers cover the rest of a chat
surface.
In an app (multi-user hosts)
An app that serves many users mounts mux.handle from
@rohansguliani/mux-chat-runtime
and gives each browser a short-lived grant instead of a runtime credential; the
runtime confines every request made with it to that user's workspace.
createAppChat is a signed-in user's whole chat without a UI: it fetches the
grant (renewing it before it expires), opens the user's chat (the stored one
or a new one), sends, and reports tool calls that finish. Scripts, bots, and
custom UIs read store and view().
import { createAppChat, sessionTurns } from "@rohansguliani/mux-chat-core"
const chat = createAppChat({ url: "/api/mux", onToolResult: ({ readOnly }) => { if (!readOnly) refresh() } })
await chat.start() // grant + chat, ready to send
await chat.send("Add NVDA to my Tech list", { agent: "assistant" })
chat.subscribe(() => render(sessionTurns(chat.store.state(), chat.sessionID()!)))createAppClient is the lower level: the grant and a runtime client
authorized by it.
The rules an app chat follows live in one place, used by createAppChat and
by @rohansguliani/mux-chat-ui's pieces alike: createGrantKeeper (renew
at 80% of the grant's life, back off from 15s to a minute through outages),
openStoredOrNew (the user's last chat if it is still theirs, else a new
one), and createToolResultTracker (report tool calls that finish while the
chat is open, never history). Each takes the caller's own requests.
Sign in with Mux (browser)
createMuxAccountClient() talks to your app's @rohansguliani/mux-chat-runtime
mount: account() (who is signed in), models() (the models they chose in
Mux), signInHref(returnTo), signOut() (which also disconnects the app in
Mux), and manage(page, onDone), which opens a standalone Mux page in a popup
(no Mux app around it) and calls onDone when the user is finished:
"providers": their providers and models."machines": what's connected to their Mux account (machines and apps), where they can disconnect apps.
Nothing secret reaches the page.
Development
From the repository root (npm ci there installs every package's toolchain):
npm run typecheck -w @rohansguliani/mux-chat-core # strict, src and tests
npm test -w @rohansguliani/mux-chat-core # vitest, plain Node
npm run build -w @rohansguliani/mux-chat-core # dist/index.js (ESM bundle) + .d.tsNo runtime dependencies. Mux's own frontend uses it for its transcript
mechanics (through Solid stores), and @rohansguliani/mux-chat-ui is built
from that frontend; use it directly for a custom UI or a non-browser client
(StockPulse's e2e/headless.mjs uses createAppChat).
