@qumra/jawab-ai
v0.1.3
Published
The assistant reply contract — typed blocks instead of raw text. Zero dependencies, no React.
Readme
@qumra/jawab-ai
The assistant reply contract — typed blocks instead of raw text. Zero dependencies, no React.
The backend builds a reply with it, the frontend renders from it, and both break at compile time when they disagree, because the types are the same types.
npm i @qumra/jawab-aiWhy blocks, not markdown
A model that answers "sales are up 12%" as prose gives the UI nothing to work with: no way to format the number for the reader's locale, no way to make the figure tappable, no way to tell a metric from a caveat.
A typed block does all three, and the contract is small enough to read in one sitting.
import { reply, text, metric, action, validate } from '@qumra/jawab-ai'
const answer = reply('r_1', [
text('Sales are up **12%** today.'),
metric([{ label: 'Sales', value: 'EGP 4,820' }]),
action([{ label: 'Open the report', intent: 'report.open' }]),
])
validate(answer) // throws with the exact failing field pathStrict when sending, tolerant when receiving
Two validators, on purpose.
validate() is strict — for the backend, before it sends. An unknown
block type here is a programming mistake: the backend cannot send a type it
does not itself know.
parseReply() is tolerant — for the client, as it receives. An unknown
block here is not a mistake, it is a reply newer than the app. There are
mobile clients in the wild that cannot be forced to update, and rejecting a
whole reply over one new block would break every old client the moment the
backend ships.
const shown = parseReply(fromServer) // drops what it cannot render, keeps the restStreaming
reduce() folds events into a reply as they arrive, so the UI renders a
half-written answer without special cases. Out-of-order events, patches
against blocks that do not exist yet, and chunks the network cut in half are
all handled — see the tests for the exact guarantees.
import { reduce, parseSSE, open, block, patch, done } from '@qumra/jawab-ai'
let state = null
for (const event of events) state = reduce(state, event)replayReply() turns a static reply into timed events, so a screen with no
backend yet exercises the same code path as the real thing.
Rendering
This package draws nothing. The React renderer lives in
@qumra/fanar/ai — it needs a
design system, and a wire contract should not.
Documentation
Source and decision log live in the repository. Comments there are written in Egyptian Arabic: they are the record of why each decision was made, kept in the language the team argued them in.
License
MIT
