npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lua-ai-global/chat-contract

v0.2.0

Published

The contract between Lua's server and every chat surface: the `::: form` engine (Lua Form v1) and whether a turn reached the server.

Readme

@lua-ai-global/chat-contract

The contract between Lua's server and every chat surface. It ships no UI and no network code; each app draws with its own design system and sends with its own transport. Formerly @lua-ai-global/chat-blocks.

npm install @lua-ai-global/chat-contract

Import from a subpath. There is no root entry, so a client loads only what it uses:

| Subpath | What | | ------------------------------------- | ------------------------------------------------------------------------------------------------ | | @lua-ai-global/chat-contract/form | The ::: form engine (Lua Form v1). Pulls in yaml: import it lazily where bundle size matters | | @lua-ai-global/chat-contract/turns | Whether a turn reached the server, and what the person sees on their message | | @lua-ai-global/chat-contract/blocks | How every surface reads the ::: blocks agents write. No dependencies |

A form

An agent, or a tool the agent calls, writes a form inline in its reply. The body is YAML (JSON also parses):

::: form
id: counters-check
title: Counter check
fields:
  - type: choice
    key: counters
    label: Counters clean?
    options: [Pass, Fail, N/A]
    required: true
  - type: textarea
    key: issue
    label: What's wrong?
    visible_if: { field: counters, equals: Fail }
  - type: photo
    key: issue_photo
    label: Photo of the issue
    max_files: 3
    visible_if: { field: counters, equals: Fail }
submit: Submit check
:::

When the user submits, the client sends one user message:

::: form-response
{"form":"counters-check","status":"submitted","values":{"counters":"Fail","issue":"Sticky residue"}}
:::

Rendering a form

import {
  parseFormBlock,
  createFormState,
  reduceFormState,
  visibleNodes,
  shownError,
  canSubmit,
  buildResponse,
  formStatus,
  formFallbackText,
  FORMS_CAPABILITY,
} from '@lua-ai-global/chat-contract/form';

const result = parseFormBlock(body); // the text between `::: form` and `:::`, verbatim
if (!result.ok) return show(result.fallbackText ?? 'This form can’t be shown here.');

let state = createFormState(result.form);
state = reduceFormState(result.form, state, { type: 'change', key: 'counters', value: 'Fail' });
visibleNodes(result.form, state.values); // the nodes to draw, in order
state = reduceFormState(result.form, state, { type: 'submit' });
if (canSubmit(state)) send(buildResponse(result.form, state.values, { tz }));
  • formStatus(form.id, laterMessages) says whether the form is open, submitted or declined (with the response), superseded, or unknown (newer history not loaded, so render it read-only).
  • A client that renders forms sends FORMS_CAPABILITY (forms-v1) in clientCapabilities.
  • Channels that can't draw a form send formFallbackText(form).

Field types

| type | value | notes | | ----------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------ | | heading, subheading, paragraph, caption | none | text (paragraph and caption are markdown) | | image | none | src (https), alt | | link | none | text, url (https, mailto, tel) | | divider | none | none | | text | string | format: plain, email, phone, url, password, passcode. Also min_length, max_length, pattern | | textarea | string | min_length, max_length | | number | number | min, max, step | | date | YYYY-MM-DD | min/max (a date, today, today+N), unavailable | | date_range | {start, end} | min, max, min_days, max_days. Both ends are inclusive | | time | HH:mm | min, max, step_minutes | | datetime | YYYY-MM-DDTHH:mm | min, max | | choice | string, or string[] when multiple | options, multiple, appearance (radio, checkbox, dropdown, chips), min_selected, max_selected | | boolean | true/false | appearance (checkbox, switch, yes_no), link | | rating | integer | min, max | | file | [{url, name, media_type, size}] | accept, max_files, max_size_mb, source |

Keys every input takes:

  • key, label, help, placeholder
  • required, default, read_only, error
  • row
  • visible_if

visible_if is a structured condition:

  • A leaf is { field, equals | not_equals | in | not_in | gt | gte | lt | lte | contains: value } or { field, empty: true|false }.
  • Leaves combine with all, any and not.

Aliases. Common names map to the types above: email, phone, radio, dropdown, checkbox, photo, document, yes_no and others.

Older pipe-syntax forms still parse.

Forward compatibility

  • Versioning. version and requires let later versions add features. A client that can't honour them shows fallback_text.
  • Reserved now: screens, ref, data, context, locale and style, plus the node types group and repeat. v1 clients reject forms that use them, and those forms fall back to text.
  • Unknown node types:
    • Optional ones are dropped.
    • A required one makes the form fall back.
    • A node can name its own fallback.

Turns

A client mints a turn id once, when it seals a turn (check it with isTurnId), and sends it on every attempt as the Idempotency-Key header and the body's clientTurnId. The server echoes it on the reply's start chunk (messageMetadata.clientTurnId) and on the user's row in history.

  • A resend with the same key never runs the turn twice. 409 TURN_IN_FLIGHT means it is still running, and 409 TURN_ALREADY_PROCESSED means it finished. deliveryFromResend reads both.
  • A resend of a finished turn is answered with its stored reply, and the start chunk says replayed: true (TurnStartMetadata). 409 TURN_ALREADY_PROCESSED remains for a reply that can't be rebuilt.
  • GET /chat/stream/:agentId/turns/:clientTurnId answers TurnStatusResponse without side effects. deliveryFromStatus turns it into what the person sees.
  • The history page lists TURN_IDEMPOTENCY_CAPABILITY in serverCapabilities when the server dedupes keyed turns.

| DeliveryState | Meaning | | --------------- | --------------------------------------------------------- | | waiting | Still on the device: offline, or queued behind a reply | | sending | On its way, and no reply chunk yet | | sent | The server has it: the reply started, or it is in history | | still-working | The connection dropped while the agent kept going | | not-sent | It never arrived, and it won't be sent again on its own | | check | It may have arrived, and the server couldn't be asked yet |

A 200 on the request means only that the server has the request. The turn can still be merged into a batch or refused before it runs, so sent waits for the first reply chunk.

Blocks

Agents write ::: blocks in their replies: actions, list-item, horizontal-list-item, images, links, documents, payment, reaction, flow, navigate, hide, form and form-response. /blocks is the one grammar every chat surface and channel reads them with.

import { parseMessage } from '@lua-ai-global/chat-contract/blocks';

parseMessage('Pick one:\n::: actions\n- Yes\n- No\n:::');
// [{ kind: 'text', text: 'Pick one:' }, { kind: 'actions', items: ['Yes', 'No'], closed: true }]
  • parseMessage(text, { streaming }) returns what to draw, in order. It drops hide blocks, shows an unknown block's body as text, drops a block with nothing usable in it, and while streaming returns { kind: 'pending' } for a block whose closer hasn't arrived (draw a placeholder, except for hide, reaction and navigate).
  • splitBlocks returns the raw segments, and stripBlocks(text, names) removes blocks and keeps the rest of the text as written.
  • The body readers (parseActions, parseCard, parseImages, …) and flowText are exported for surfaces that render a single kind.

The grammar:

  • A marker may use any spacing or case, and may be indented: ::: actions, :::actions and ::: Actions are the same. A line of only colons (:::, ::, ::::) closes a block.
  • ::: reaction emoji=👍 ::: on one line is a whole block. Text after an opener is the block's first line.
  • An opener or closer glued to text at the end of a line counts (Sure!::: actions, - Yes:::). Any other ::: in the middle of a line is text.
  • A new opener closes a block that is still open. A lone closer is dropped. Anything inside a code fence is code.
  • A block still open when a finished message ends renders. An unclosed item block keeps only its own lines, and the prose after them is text.
  • A hide block ends only at a column-0 closer, so hidden context can carry indented ::: lines.
  • A literal \n outside code is a line break. Form bodies are kept verbatim.
  • Card headings work with or without a space (#Title, ## Sub). Links allow http(s), mailto: and tel:. Images, payment and documents allow http(s) only. Navigate takes an app route or an http(s) URL.

React Native and Expo

The package is plain ES2020 and runs under Hermes. Every subpath has a CommonJS build for jest. Under jest-expo, add yaml to transformIgnorePatterns, because jest resolves yaml's ESM browser build.

Fixtures

@lua-ai-global/chat-contract/fixtures/forms/corpus.json holds sample bodies covering every type, with the expected parse outcome. Renderer tests can use them as a smoke suite.

@lua-ai-global/chat-contract/fixtures/blocks/corpus.json holds one message per grammar rule, with the expected splitBlocks segments. Run it through your own adapter to check a surface reads blocks the same way as every other.