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;
};
MemberContract
versionLiteral protocol version 1
getStateRead the current spec, selected id, transcript, pending user messages, and bounded activity history
blocksRead the 55-entry detached serialisable metadata catalog without transferring Svelte components
applyOpsApply a batch and receive an explicit success flag plus all reported errors
reportActivitySubmit one runtime-validated validation, operation, or agent activity
setSpecReplace the current builder specification
sayAppend an agent chat message
askReturn the oldest unanswered user message, or null
exportCodeProject the current specification to Svelte source
onUserMessageSubscribe 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 request entry and a queued reply entry; an agent reply resolves the waiting request;
  • applyOps records 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 persistence state.

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:

  • kind must be validation, operation, or agent;
  • status must be queued, active, done, or error;
  • label must be non-blank and no longer than 120 characters;
  • optional detail must be no longer than 500 characters; and
  • unknown fields are rejected. An input such as chainOfThought therefore 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:

  1. relay subscribers registered with onUserMessage receive its ChatMessage; and
  2. document dispatches a bits-builder:user-message CustomEvent whose detail is that new ChatMessage.

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:

  1. Wait for document.documentElement.dataset.bitsBuilder === 'ready'.
  2. Subscribe with onUserMessage, or listen for bits-builder:user-message and then call ask.
  3. Read getState() and blocks() before constructing operations.
  4. Translate the user request into the smallest ordered BuilderOp[] batch.
  5. If useful, call reportActivity(...) with a concise observable validation, operation, or agent status—never hidden reasoning.
  6. Call applyOps(ops).
  7. If ok is 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.
  8. Call say(...) with a concise, accurate outcome. The relay method accepts text only; it does not accept an ops argument.
  9. Re-read state before a dependent second edit rather than assuming an id or tree position.
  10. 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.


loca.zone · bits.loca.zone · Svelte