A2UI · Reference

Parser, Resolver, and Guards

@threadplane/a2ui exports small helpers that keep stream parsing and dynamic value resolution consistent across packages.

createA2uiMessageParser()

import { createA2uiMessageParser } from '@threadplane/a2ui';
 
const parser = createA2uiMessageParser();
const messages = parser.push(chunk);

push(chunk) appends the chunk to an internal buffer and returns every complete message found before the last newline.

Important behavior from source:

  • the parser is JSONL-based;
  • a complete message requires a trailing newline;
  • CRLF works because each line is trimmed;
  • empty lines are ignored;
  • malformed lines are skipped silently;
  • unknown top-level envelopes are ignored (forward compatibility with future protocol versions, e.g. v1.0 message kinds);
  • a missing version field defaults to 'v0.9'; a present version is preserved;
  • multiple messages can be returned from one chunk.

The recognized envelope keys are createSurface, updateComponents, updateDataModel, and deleteSurface. The parser checks only for a known envelope key and a non-null object value. It does not validate each nested field.

resolveDynamic()

import { resolveDynamic } from '@threadplane/a2ui';
 
const model = {
  customer: { name: 'Ada' },
  count: 2,
};
 
resolveDynamic({ path: '/customer/name' }, model); // "Ada"
resolveDynamic(2, model); // 2

resolveDynamic(value, model, scope?) handles:

Input shapeResult
bare literal (string, number, boolean)returned as-is
{ path }the value at that model path
{ call }executes via the A2uiFunctionRegistry passed as the fourth argument (createA2uiFunctionRegistry() provides the standard set); undefined without a registry or for unknown names
arraysrecursively resolved array values
null or undefinedreturned as-is
unrecognized plain objectsreturned as-is

Resolution order is fixed: { call } function calls are checked before { path } references (so a call's args never masquerade as a binding), then path refs resolve, then everything else passes through as a bare literal.

Absolute paths start with /.

Relative paths resolve against scope.basePath when a scope is supplied. Without a scope, a relative path is treated as root-relative by prefixing /.

resolveDynamic(
  { path: 'name' },
  { items: [{ name: 'Ada' }] },
  { basePath: '/items/0', item: { name: 'Ada' } },
); // "Ada"

A2uiScope.item is part of the public type, but the current resolver only uses basePath.

Pointer helpers

import { getByPointer, setByPointer, deleteByPointer } from '@threadplane/a2ui';

The pointer helpers use slash-separated paths:

const model = { customer: { name: 'Ada' } };
 
getByPointer(model, '/customer/name'); // "Ada"
setByPointer(model, '/customer/name', 'Grace');
deleteByPointer(model, '/customer/name');

Current behavior is intentionally small:

  • empty pointer and / point at the root;
  • missing paths read as undefined;
  • setByPointer() returns a cloned object path rather than mutating the original root;
  • deleteByPointer() returns the original model when the parent path does not exist;
  • deleteByPointer() on an array index sets it to undefined and preserves the array's length (the v0.9 array-delete rule).

These helpers do not implement full RFC 6901 escaping semantics. Avoid keys that require ~0 or ~1 escaping unless you normalize them before they enter A2UI state.

Guards

The public guards are:

isPathRef(value)      // narrows to { path: string }
isFunctionCall(value) // narrows to { call: string; args?: Record<string, unknown> }

isPathRef() verifies the value is an object with a string path; isFunctionCall() verifies an object with a string call. Bare literals need no guard in v0.9 — a value that matches neither guard is a literal (or an unrecognized object that passes through unchanged).

Use them when you need to branch on protocol values without importing internal renderer code.

Validation vs handler wiring

This package does not run validation rules, map actions to Angular handlers, or call user functions. It gives you typed values and parsing helpers.

A practical boundary is:

  • use @threadplane/a2ui to parse and inspect the protocol stream;
  • use app or server validation to decide whether a message is trusted;
  • use @threadplane/chat and @threadplane/render to display surfaces and wire interactions.

That split keeps protocol parsing deterministic and keeps privileged behavior in the host application.