Agent Relay Protocol
src/lib/builder/relay.ts implements one versioned browser surface at window.bitsBuilder, installed by the builder route. The protocol remains version 1 with exactly the same ten public members: the literal version plus nine callable methods. This refinement changes only additive block metadata and the catalog size, from 22 entries to 55; it changes no relay member, activity contract, readiness marker, or message signal.
Version 1 API
The relay-visible activity and state types below are copied from src/lib/builder/spec.ts:
export type BuilderActivityStatus = 'queued' | 'active' | 'done' | 'error';
export type BuilderActivityKind =
| 'request'
| 'validation'
| 'operation'
| 'persistence'
| 'agent';
/** A concise, observable builder action. Never use this for hidden reasoning. */
export type BuilderActivity = {
id: string;
kind: BuilderActivityKind;
status: BuilderActivityStatus;
label: string;
detail?: string;
/** epoch ms */
at: number;
};
/** Runtime-validated payload accepted from browser-relay agents. */
export type BuilderActivityInput = {
kind: 'validation' | 'operation' | 'agent';
status: BuilderActivityStatus;
label: string;
detail?: string;
};
/** Everything the relay API hands an agent in one read. */
export type BuilderState = {
version: typeof SPEC_VERSION;
spec: BuilderSpec;
selectedId: NodeId | null;
messages: ChatMessage[];
/** user messages with no agent reply after them */
pending: ChatMessage[];
/** bounded, observable action history */
activities: BuilderActivity[];
};The public relay type below is copied from src/lib/builder/relay.ts:
export type BitsBuilderRelay = {
version: 1;
getState: () => BuilderState;
blocks: () => BlockMeta[];
applyOps: (ops: BuilderOp[]) => { ok: boolean; errors: string[] };
reportActivity: (activity: BuilderActivityInput) => { ok: boolean; errors: string[] };
setSpec: (spec: BuilderSpec) => void;
say: (text: string) => void;
ask: () => ChatMessage | null;
exportCode: () => string;
onUserMessage: (callback: (message: ChatMessage) => void) => () => void;
};| Member | Contract |
|---|---|
version | Literal protocol version 1 |
getState | Read the current spec, selected id, transcript, pending user messages, and bounded activity history |
blocks | Read the 55-entry detached serialisable metadata catalog without transferring Svelte components |
applyOps | Apply a batch and receive an explicit success flag plus all reported errors |
reportActivity | Submit one runtime-validated validation, operation, or agent activity |
setSpec | Replace the current builder specification |
say | Append an agent chat message |
ask | Return the oldest unanswered user message, or null |
exportCode | Project the current specification to Svelte source |
onUserMessage | Subscribe to user messages; its return value unsubscribes |
The relay surface has exactly ten public members: the literal version plus nine callable methods. Its member set and signatures are unchanged; version 1 consumers need no protocol migration for the additive metadata or larger catalog.
Additive catalog metadata
The only relay-visible additions are BlockMeta.showcaseSlug; the date, time, datetime-local, records, and node-ref field kinds; BlockField.description, columns, and accepts; and the new BlockRecordColumn type. blocks() now returns 55 addable block types = 46 showcase mappings + 9 builder-only definitions. The 22 original type keys remain registered.
The serialisable metadata types below are copied from src/lib/builder/spec.ts:
/** One editable column inside a `records` field. */
export type BlockRecordColumn = {
key: string;
label: string;
kind: 'text' | 'textarea' | 'boolean' | 'select' | 'items';
/** for kind === 'select' */
options?: string[];
};
/** Serialisable block metadata exposed to agents via `window.bitsBuilder.blocks()`. */
export type BlockField = {
key: string;
label: string;
kind:
| 'text'
| 'textarea'
| 'number'
| 'boolean'
| 'select'
| 'items'
| 'date'
| 'time'
| 'datetime-local'
| 'records'
| 'node-ref';
/** for kind === 'select' */
options?: string[];
min?: number;
max?: number;
step?: number;
/** inline editor help rendered under the control */
description?: string;
/** for kind === 'records' */
columns?: BlockRecordColumn[];
/** for kind === 'node-ref': block types this reference may point at */
accepts?: string[];
};
export type BlockMeta = {
type: string;
label: string;
category: string;
container: boolean;
/** bits-ui namespace this block is built from, when applicable */
bits?: string;
/** showcase registry slug this block mirrors, when it mirrors one */
showcaseSlug?: string;
description: string;
defaults: Record<string, unknown>;
fields: BlockField[];
};Detached relay metadata
blocks() delegates to blockMetas(). Each call strips the internal Svelte component and createDefaults factory, materialises createDefaults() into defaults, and returns a structuredClone. Mutating a nested record default, columns, or a nested column options array therefore cannot affect the registry or Inspector metadata.
The agent-facing invariant is exact: each record’s materialised default key set equals its fields[].key set. Registry initialisation rejects duplicate field keys, missing defaults, extra defaults, and factory keys that have no field. Temporal defaults and edits cross the relay as ISO strings only; @internationalized/date objects exist only behind the conversion boundary.
Observable activity contract
Activity is a concise account of observable actions, statuses, and results. It must never contain chain-of-thought, hidden reasoning, private deliberation, or an agent’s internal scratch work.
The builder records its own activity where it owns the event:
- a user message creates a completed
requestentry and a queued reply entry; an agent reply resolves the waiting request; applyOpsrecords validation, each ordered operation, and the batch result, including structured failures;- undo and redo record their observable operation result; and
- session restore and save record
persistencestate.
request and persistence are automatic sources and cannot be supplied through reportActivity. Relay agents may report only validation, operation, or agent activity. The input is a strict runtime-validated object:
kindmust bevalidation,operation, oragent;statusmust bequeued,active,done, orerror;labelmust be non-blank and no longer than 120 characters;- optional
detailmust be no longer than 500 characters; and - unknown fields are rejected. An input such as
chainOfThoughttherefore returns{ ok: false, errors: [...] }rather than entering the activity stream.
Successful reports are trimmed, assigned their store-generated id and epoch-millisecond at, appended once, and return { ok: true, errors: [] }. Invalid reports append nothing and return every validation error.
Activity retains the newest 120 entries. The browser persists that bounded list client-side under bits-builder:activity:v1, restores only valid entries, and continues in memory if local storage is unavailable. Activity is deliberately absent from the server session payload, which remains exactly { spec, messages }.
Readiness and signals
installRelay(store) installs the API and returns a teardown function. While installed, the root <html> element carries:
data-bits-builder="ready"Every new user message has two browser-visible signals:
- relay subscribers registered with
onUserMessagereceive itsChatMessage; and documentdispatches abits-builder:user-messageCustomEventwhosedetailis that newChatMessage.
An agent may consume the typed message from event.detail, onUserMessage, ask, or getState. The event and subscription are push signals; ask remains the ordered query for the oldest unanswered user message.
Agent-relay loop
flowchart LR User["User"] -->|submits request| UI["Builder UI"] UI -->|records user message| Relay["window.bitsBuilder"] Relay -->|event and subscription callback| Agent["OMP browser-relay agent"] Agent -->|ask, getState, blocks| Relay Agent -->|applyOps, reportActivity| Relay Relay -->|reactive store update| UI Agent -->|say| Relay UI -->|preview and transcript| User
A reliable agent loop is:
- Wait for
document.documentElement.dataset.bitsBuilder === 'ready'. - Subscribe with
onUserMessage, or listen forbits-builder:user-messageand then callask. - Read
getState()andblocks()before constructing operations. - Translate the user request into the smallest ordered
BuilderOp[]batch. - If useful, call
reportActivity(...)with a concise observable validation, operation, or agent status—never hidden reasoning. - Call
applyOps(ops). - If
okis false, inspect every error and re-read state before reporting the outcome. A batch is ordered but not transactional, so valid earlier operations may already have applied. - Call
say(...)with a concise, accurate outcome. The relay method accepts text only; it does not accept anopsargument. - Re-read state before a dependent second edit rather than assuming an id or tree position.
- Invoke the unsubscribe function when the agent no longer needs message callbacks.
Direct replacement and export
setSpec is available for a complete versioned document supplied by the agent, but its void contract does not expose validation errors. Incremental work should prefer applyOps because its result reports errors and successful spec mutations enter the same history and persistence path as UI edits.
exportCode returns source derived from current state. It does not write a route or deploy an application; those would be separate, explicitly authorized actions.
Verification state
The current source contract is version 1 with the same ten relay members and a 55-entry detached metadata catalog. Dated behavioral and deployment receipts are recorded in private page (sign-in required); its earlier 22-record receipts remain historical snapshots.