Page actions

React AG-UI A2UI preview

Turn a flight request into a form the traveler can edit. A2UI describes the surface and its data; the application renders an allowed catalog and sends explicit user actions back to the backend.

The native React APIs on this page are experimental, private source-build packages. Run the repository example to use them; they are not an npm installation recipe.

Keep the received surface separate from edits

The server sends a surface description and data-model updates. React retains the received surface, while form controls hold the traveler’s current edits. Search, Select, and Modify send intentional actions; typing does not start another agent run. The example admits a fixed flight-form profile rather than every possible A2UI component.

The React conversation walkthrough connects this capability to requests, tools, UI state, and saved history.

Start a conversation

Choose Run and send a flight request. Try a flight request fills the composer without submitting it. The first Send creates the native session. Mounting, Docs, Code, frontend selection and mode navigation send no agent requests. New conversation clears the local view and waits for Send. Mode navigation preserves the mounted frame and an unsent draft; reload starts a fresh local view.

app.tsx
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import { ExampleFooter, ExampleLayout } from '../../../../shared/react-example';
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
import {
  ChatInput,
  MessageList,
  type ChatInputHandle,
} from '@threadplane/react/chat';
import type { A2uiApplication } from './application';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Same-layout private presentation source in installed consumers.
import { A2uiSurface } from '../../../../../libs/cockpit-a2ui-react/src/index';
 
function humanText(content: string) {
  try {
    const value = JSON.parse(content);
    if (value.version === 'v0.9') {
      const labels: Record<string, string> = {
        bookingSubmit: 'Search flights',
        flightSelect: 'Select demonstration flight',
        modifySearch: 'Modify search',
      };
      if (Object.hasOwn(labels, value.action?.name))
        return labels[value.action.name];
    }
  } catch {
    /* Ordinary human text. */
  }
  return content;
}
function SavedText({ content }: { readonly content: string }) {
  const at = content.indexOf('---a2ui_JSON---');
  return at < 0 ? (
    <p className="a2ui-prose">{content}</p>
  ) : (
    <>
      {content.slice(0, at).trim() && (
        <p className="a2ui-prose">{content.slice(0, at).trim()}</p>
      )}
      <details className="a2ui-wire">
        <summary>Response details</summary>
        <pre>{content.slice(at)}</pre>
      </details>
    </>
  );
}
/** Main owns the application. React only observes it and borrows its commands. */
export function A2uiDemo({
  application,
  onReady,
}: {
  readonly application: A2uiApplication;
  readonly onReady: () => void;
}) {
  const snapshot = useSyncExternalStore(
    application.subscribe,
    application.getSnapshot
  );
  const [draft, setDraft] = useState('');
  const composer = useRef<ChatInputHandle>(null);
  useEffect(onReady, [onReady]);
  const status = snapshot.busy
    ? snapshot.activity === 'replacing'
      ? 'Starting a new conversation…'
      : 'Waiting for the confirmed preview…'
    : snapshot.error
    ? 'Start a new conversation to continue.'
    : snapshot.messages.length
    ? 'Preview confirmed.'
    : 'Ready.';
  return (
    <ExampleLayout className="a2ui-demo">
      <header>
        <div>
          <h1>Plan a flight</h1>
        </div>
        <button
          type="button"
          onClick={() => {
            setDraft('');
            void application.newConversation().catch(() => undefined);
          }}
        >
          New conversation
        </button>
      </header>
      <p>
        Flight preview: request a flight form, edit your details, and select a
        demonstration flight. This preview makes no booking or payment.
      </p>
      {snapshot.messages.length === 0 && (
        <button
          className="a2ui-suggestion"
          type="button"
          disabled={snapshot.busy || !snapshot.canSubmit}
          onClick={() => {
            setDraft('I want to fly LAX to JFK');
            composer.current?.focus();
          }}
        >
          Try a flight request
        </button>
      )}
      <MessageList
        rows={[]}
        label="Conversation"
        className="a2ui-conversation"
        key={`messages:${snapshot.viewGeneration}`}
      >
        {!snapshot.messages.length && (
          <p className="a2ui-empty">
            Your confirmed flight previews will appear here.
          </p>
        )}
        {snapshot.messages.map((message) => {
          const surface = snapshot.surfaces.find(
            (s) => s.messageId === message.id
          );
          return (
            <article
              key={message.id}
              className={`tp-chat-message tp-chat-message--${message.role} a2ui-message a2ui-message--${message.role}`}
              aria-label={message.role === 'user' ? 'You' : 'Assistant'}
            >
              <p className="tp-chat-message__role a2ui-message-role">
                {message.role === 'user' ? 'You' : 'Assistant'}
              </p>
              {surface ? (
                <A2uiSurface
                  surface={surface}
                  active={
                    snapshot.activeSurface === surface.id &&
                    !snapshot.busy &&
                    snapshot.canSubmit
                  }
                />
              ) : message.role === 'user' ? (
                <p className="a2ui-prose">{humanText(message.content)}</p>
              ) : (
                <SavedText content={message.content} />
              )}
            </article>
          );
        })}
      </MessageList>
      {snapshot.error && snapshot.native && (
        <details className="a2ui-wire">
          <summary>Unconfirmed response details</summary>
          <pre>
            {JSON.stringify(snapshot.native.transcript, null, 2).slice(
              0,
              100000
            )}
          </pre>
        </details>
      )}
      <ExampleFooter>
        {snapshot.error && <p role="alert">{snapshot.error}</p>}
        <ChatInput
          ref={composer}
          key={snapshot.viewGeneration}
          label="Message"
          value={draft}
          onValueChange={setDraft}
          placeholder="Ask for a flight form…"
          hint="Enter to send. Shift+Enter adds a new line."
          busy={snapshot.busy}
          disabled={!snapshot.canSubmit && !snapshot.busy}
          onStop={
            snapshot.busy
              ? () => {
                  void application.stop().catch(() => undefined);
                }
              : undefined
          }
          onSubmit={(text) => {
            const state = application.getSnapshot();
            if (state.busy || !state.canSubmit) return false;
            // ChatInput clears this admitted draft synchronously, independently of settlement.
            void application.submit(text).catch(() => undefined);
            return true;
          }}
        />
        <p role="status">{status}</p>
      </ExampleFooter>
    </ExampleLayout>
  );
}
main.tsx
import { StrictMode } from 'react';
import { createRoot, type Root } from 'react-dom/client';
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this private bridge.
import {
  GENERATED_RUNTIME_PARENT_ORIGINS,
  installRuntimeBridge,
  type InstalledRuntimeBridge,
} from '../../../../../libs/cockpit-runtime-bridge/src/index';
import { A2uiDemo } from './app';
import {
  a2uiConnection,
  createConnectedApplication,
  type A2uiConnection,
} from './connection';
import type { A2uiApplication } from './application';
import '@threadplane/react/chat/styles.css';
import './styles.css';
// eslint-disable-next-line @nx/enforce-module-boundaries -- The isolated consumer copies this internal example layout.
import '../../../../shared/react-example.css';
 
/** Own bootstrap and teardown outside React's replayable presentation lifetime. */
export function startA2uiDemo(
  element: HTMLElement,
  dependencies: {
    readonly bridge?: InstalledRuntimeBridge;
    readonly root?: Pick<Root, 'render' | 'unmount'>;
    readonly createApplication?: (
      connection: A2uiConnection
    ) => A2uiApplication;
  } = {}
) {
  const bridge =
    dependencies.bridge ??
    installRuntimeBridge(undefined, {
      allowedParentOrigins: GENERATED_RUNTIME_PARENT_ORIGINS,
    });
  let root: Pick<Root, 'render' | 'unmount'>;
  try {
    root = dependencies.root ?? createRoot(element);
  } catch (error) {
    try {
      bridge.dispose();
    } catch {
      /* Preserve the acquisition error. */
    }
    throw error;
  }
  const createApplication =
    dependencies.createApplication ?? createConnectedApplication;
  let active = true,
    markedReady = false,
    application: A2uiApplication | undefined,
    cleanup: Promise<void> | undefined;
  function dispose(): Promise<void> {
    if (!active) return cleanup ?? Promise.resolve();
    active = false;
    window.removeEventListener('pagehide', pagehide);
    // Revoke work/stop first, then remove borrowed views, then dispose owned data.
    try {
      application?.revoke();
    } catch {
      /* Every acquired owner still needs cleanup. */
    }
    try {
      root.unmount();
    } catch {
      /* Unmount failure cannot retain application authority. */
    }
    cleanup = Promise.resolve()
      .then(() => application?.dispose())
      .catch(() => undefined)
      .then(() => {
        try {
          bridge.dispose();
        } catch {
          /* Page teardown cannot trigger unhandled rejection. */
        }
      });
    return cleanup;
  }
  function pagehide() {
    void dispose();
  }
  window.addEventListener('pagehide', pagehide);
  const ready = bridge
    .awaitConfiguration()
    .then(async (configuration) => {
      if (!active) return;
      const connection = a2uiConnection(configuration);
      application = createApplication(connection);
      if (!active) {
        application.revoke();
        await application.dispose();
        return;
      }
      root.render(
        <StrictMode>
          <A2uiDemo
            application={application}
            onReady={() => {
              if (active && !markedReady) {
                markedReady = true;
                bridge.markReady();
              }
            }}
          />
        </StrictMode>
      );
    })
    .catch(async () => {
      if (!active) return;
      try {
        bridge.markError('bootstrap_failed');
      } catch {
        /* Continue local cleanup. */
      }
      let renderFailed = false;
      try {
        root.render(<p role="alert">The flight preview could not start.</p>);
      } catch {
        renderFailed = true;
      }
      if (application || renderFailed) await dispose();
    });
  return Object.freeze({ ready, dispose });
}
const element = document.getElementById('root');
if (element) startA2uiDemo(element);
connection.ts
import { createA2uiApplication } from './application';
import { createSession } from '@threadplane/ag-ui';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Same-layout private bridge.
import type { RuntimeBridgeConfiguration } from '../../../../../libs/cockpit-runtime-bridge/src/index';
export interface A2uiConnection {
  readonly url: string;
  readonly label: 'Shared runtime' | 'Developer runtime';
}
function endpoint(value: string): string {
  try {
    const url = new URL(value);
    if (
      !['http:', 'https:'].includes(url.protocol) ||
      url.username ||
      url.password
    )
      throw Error('Unavailable');
    return url.href;
  } catch {
    throw Error('Runtime configuration failed.');
  }
}
/** Select only the shared route or an authored credential-free native endpoint. */
export function a2uiConnection(
  configuration: RuntimeBridgeConfiguration
): A2uiConnection {
  if (
    configuration.status === 'error' ||
    (configuration.status === 'configured' &&
      configuration.target.kind === 'langsmith')
  )
    throw Error('Runtime configuration failed.');
  const target =
    configuration.status === 'configured' &&
    configuration.target.kind === 'ag-ui'
      ? configuration.target
      : null;
  return Object.freeze({
    url: endpoint(
      target?.endpoint ??
        new URL('/ag-ui/a2ui/agent', window.location.origin).href
    ),
    label: target ? 'Developer runtime' : 'Shared runtime',
  });
}
export function createConnectedApplication(connection: A2uiConnection) {
  const url = endpoint(connection.url);
  return createA2uiApplication({
    sessionFactory(threadId) {
      return createSession({ threadId, url });
    },
  });
}

Edit the latest confirmed preview

Only the latest settled preview is interactive. Admission requires the original successful submission, matching native root run completion, idle native state, the exact ordinary transcript and an accepted shared presentation transition. Older cards retain their last displayed edits and remain read-only. Invalid input remains visible and receives focus; replaced callbacks cannot submit.

application.ts
import type { Session } from '@threadplane/ag-ui';
import type { CompleteOutcome } from '@threadplane/core';
import {
  createNativeObservation,
  type NativeObservation,
  type NativeSnapshot,
  type OrdinaryMessage,
} from './terminal';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Internal package-less shared flight presentation.
import {
  createFlightSurface,
  copyData,
  type FlightSurface,
  type SurfaceView,
  type SurfaceKind,
} from '../../../../../libs/cockpit-a2ui-react/src/index';
export interface A2uiSnapshot {
  readonly threadId: string | null;
  readonly native: NativeSnapshot | undefined;
  readonly messages: readonly OrdinaryMessage[];
  readonly surfaces: readonly SurfaceView[];
  readonly activeSurface: string | null;
  readonly activity: 'idle' | 'running' | 'replacing';
  readonly busy: boolean;
  readonly canSubmit: boolean;
  readonly error: string | null;
  readonly outcome: CompleteOutcome | null;
  readonly viewGeneration: number;
}
/** Native Session owns protocol execution; this owner admits confirmed flight views. */
export function createA2uiApplication(options: {
  readonly sessionFactory: (threadId: string) => Session;
}) {
  type Owner = {
    readonly session: Session;
    readonly threadId: string;
    readonly runIds: Set<string>;
    release?: () => void;
    observation?: NativeObservation;
    pendingSubmit?: Promise<CompleteOutcome>;
    submitOutcome?: unknown;
    closed: boolean;
  };
  const listeners = new Set<{ notify: () => void }>(),
    surfaces = new Set<FlightSurface>();
  const sessions = new WeakSet<object>(),
    threads = new Set<string>(),
    cleanups = new Set<Promise<void>>();
  let owner: Owner | undefined,
    operation: AbortController | undefined,
    active: FlightSurface | undefined;
  let revoked = false,
    generation = 0,
    sequence = 0,
    disposal: Promise<void> | undefined;
  let snapshot: A2uiSnapshot = Object.freeze({
    threadId: null,
    native: undefined,
    messages: Object.freeze([]),
    surfaces: Object.freeze([]),
    activeSurface: null,
    activity: 'idle',
    busy: false,
    canSubmit: true,
    error: null,
    outcome: null,
    viewGeneration: 0,
  });
  function publish(change: Partial<A2uiSnapshot>) {
    if (revoked) return;
    snapshot = Object.freeze({ ...snapshot, ...change });
    for (const registration of [...listeners]) {
      if (revoked) break;
      if (listeners.has(registration))
        try {
          registration.notify();
        } catch {
          /* A view cannot interrupt ownership. */
        }
    }
  }
  function track(work: () => unknown) {
    let pending: Promise<void>;
    try {
      pending = Promise.resolve(work()).then(
        () => undefined,
        () => undefined
      );
    } catch {
      pending = Promise.resolve();
    }
    cleanups.add(pending);
    void pending.then(() => cleanups.delete(pending));
    return pending;
  }
  function close(selected: Owner, stop: boolean) {
    if (selected.closed) return;
    selected.closed = true;
    selected.observation = undefined;
    try {
      selected.release?.();
    } catch {
      /* Revoked callbacks have no authority. */
    }
    selected.release = undefined;
    // stop() is void-valued; the original pending submit owns its eventual outcome.
    if (stop) track(() => selected.session.stop());
    track(() => selected.session.dispose());
  }
  function detach(stop: boolean) {
    const selected = owner;
    owner = undefined;
    if (selected) close(selected, stop);
  }
  function retireActive() {
    const selected = active;
    active = undefined;
    selected?.dispose();
  }
  function read(selected: Owner, valid: () => boolean) {
    if (!valid()) return;
    const getSnapshot = selected.session.getSnapshot;
    if (!valid()) return;
    const value = getSnapshot.call(selected.session);
    if (!valid()) return;
    const captured = copyData(value) as unknown as NativeSnapshot;
    return valid() ? captured : undefined;
  }
  function observe(selected: Owner, valid: () => boolean) {
    if (!valid()) return;
    try {
      const native = read(selected, valid);
      if (!native || !valid()) return;
      selected.observation?.observe(native);
      if (!valid()) return;
      const identity = selected.observation?.identity();
      if (!valid()) return;
      // Bind identity before any application listener can replace or stop the owner.
      if (identity) selected.runIds.add(identity.runId);
      publish({ native });
    } catch {
      if (valid()) {
        operation?.abort();
        operation = undefined;
        retireActive();
        detach(true);
        publish({
          busy: false,
          activity: 'idle',
          canSubmit: false,
          activeSurface: null,
          error:
            'The native response is unavailable. Start a new conversation.',
        });
      }
    }
  }
  function install(valid: () => boolean): Owner | undefined {
    const threadId = crypto.randomUUID();
    if (!valid()) return;
    if (!threadId || threads.has(threadId))
      throw Error('Reused thread identity');
    threads.add(threadId);
    const sessionFactory = options.sessionFactory;
    if (!valid()) return;
    const session = sessionFactory.call(options, threadId);
    if (!session || typeof session !== 'object')
      throw Error('Unavailable native owner');
    const selected: Owner = {
      session,
      threadId,
      runIds: new Set(),
      closed: false,
    };
    if (!valid()) {
      close(selected, true);
      return;
    }
    if (sessions.has(session)) throw Error('Reused native owner');
    sessions.add(session);
    owner = selected;
    for (const method of [
      'getSnapshot',
      'subscribe',
      'submit',
      'stop',
      'dispose',
      'resume',
    ] as const) {
      if (typeof session[method] !== 'function')
        throw Error('Unavailable native owner');
      if (!valid() || owner !== selected || selected.closed) return;
    }
    const subscribe = session.subscribe;
    if (!valid() || owner !== selected || selected.closed) return;
    const release = subscribe.call(session, () => {
      const selectedOperation = operation;
      if (!selectedOperation || !selected.observation) return;
      observe(
        selected,
        () =>
          !revoked &&
          owner === selected &&
          !selected.closed &&
          operation === selectedOperation &&
          !selectedOperation.signal.aborted
      );
    });
    if (typeof release !== 'function')
      throw Error('Unavailable native observation');
    selected.release = release;
    if (selected.closed || !valid() || owner !== selected) {
      try {
        release();
      } catch {
        /* Cleanup is best effort after revocation. */
      }
      if (!selected.closed) close(selected, true);
      return;
    }
    const initial = read(selected, valid);
    if (!valid()) return;
    if (
      !initial ||
      initial.status !== 'idle' ||
      initial.transcript.length ||
      initial.subagents.length ||
      initial.decision ||
      initial.run
    )
      throw Error('Native owner is not fresh');
    return selected;
  }
  async function submit(
    text: string,
    expected: SurfaceKind = 'booking'
  ): Promise<boolean> {
    if (
      revoked ||
      operation ||
      snapshot.busy ||
      !snapshot.canSubmit ||
      typeof text !== 'string' ||
      !text.trim()
    )
      return false;
    const admitted = new AbortController();
    operation = admitted;
    const valid = () =>
      !revoked && operation === admitted && !admitted.signal.aborted;
    retireActive();
    publish({
      busy: true,
      canSubmit: false,
      activeSurface: null,
      activity: 'running',
      error: null,
      outcome: null,
    });
    let selected: Owner | undefined;
    try {
      if (!valid()) return false;
      selected = owner ?? install(valid);
      if (!selected || !valid() || selected.closed) return false;
      const chosen = selected;
      const current = () => valid() && owner === chosen && !chosen.closed;
      const observation = createNativeObservation({
        owner: chosen,
        generation: ++generation,
        threadId: chosen.threadId,
        message: text,
        previous: snapshot.messages,
        usedRunIds: [...chosen.runIds],
        valid: current,
      });
      if (!current()) return false;
      chosen.observation = observation;
      publish({ threadId: chosen.threadId });
      if (!current()) return false;
      const submitNative = chosen.session.submit;
      if (!current()) return false;
      const pending = submitNative.call(chosen.session, text, {
        signal: admitted.signal,
      });
      chosen.pendingSubmit = pending;
      // Retain the actual native outcome even when Stop/New has detached publication.
      void pending.then(
        (value) => {
          chosen.submitOutcome = value;
        },
        (error) => {
          chosen.submitOutcome = error;
        }
      );
      const outcome = await pending;
      if (!current()) return false;
      const native = read(chosen, current);
      if (!native || !current()) return false;
      const messages = observation.confirm(outcome, native);
      if (!current()) return false;
      const reported = [
        'success',
        'error',
        'aborted',
        'paused',
        'interrupted',
      ].includes(outcome)
        ? outcome
        : null;
      if (!messages) {
        detach(false);
        operation = undefined;
        publish({
          native,
          outcome: reported,
          busy: false,
          activity: 'idle',
          canSubmit: false,
          error:
            'The native response could not be confirmed. Start a new conversation.',
        });
        return false;
      }
      const last = messages.at(-1);
      if (!last) throw Error('Missing confirmed response');
      let prepared: FlightSurface;
      try {
        prepared = createFlightSurface({
          content: last.content,
          expected,
          messageId: last.id,
          id: 'surface-' + ++sequence,
          isCurrent: () =>
            !revoked &&
            active === prepared &&
            !operation &&
            !snapshot.busy &&
            snapshot.canSubmit,
          onAction(payload) {
            if (
              revoked ||
              active !== prepared ||
              operation ||
              snapshot.busy ||
              !snapshot.canSubmit
            )
              return;
            const next: SurfaceKind =
              payload.action.name === 'bookingSubmit'
                ? 'results'
                : payload.action.name === 'flightSelect'
                ? 'confirmation'
                : 'booking';
            void submit(JSON.stringify(payload), next).catch(() => undefined);
          },
        });
      } catch {
        if (current()) {
          detach(false);
          operation = undefined;
          publish({
            native,
            messages,
            outcome: reported,
            busy: false,
            activity: 'idle',
            canSubmit: false,
            error:
              'This confirmed response is unavailable. Start a new conversation.',
          });
        }
        return false;
      }
      if (!current()) {
        prepared.dispose();
        return false;
      }
      surfaces.add(prepared);
      active = prepared;
      chosen.observation = undefined;
      operation = undefined;
      // The ordinary transcript, immutable view and current command epoch publish together.
      publish({
        native,
        messages,
        surfaces: Object.freeze([...snapshot.surfaces, prepared.view]),
        activeSurface: prepared.view.id,
        outcome: reported,
        busy: false,
        activity: 'idle',
        canSubmit: true,
        error: null,
      });
      return (
        !revoked && owner === chosen && !chosen.closed && active === prepared
      );
    } catch {
      if (valid()) {
        detach(true);
        operation = undefined;
        publish({
          busy: false,
          activity: 'idle',
          canSubmit: false,
          activeSurface: null,
          error: 'The native request failed. Start a new conversation.',
        });
      }
      return false;
    } finally {
      if (valid()) {
        if (selected) selected.observation = undefined;
        operation = undefined;
        publish({ busy: false, activity: 'idle', canSubmit: false });
      }
    }
  }
  async function stop() {
    if (revoked || (!operation && !owner)) return;
    operation?.abort();
    operation = undefined;
    retireActive();
    detach(true);
    publish({
      busy: false,
      activity: 'idle',
      activeSurface: null,
      canSubmit: false,
      error: 'Stopped. Start a new conversation to continue.',
    });
  }
  async function newConversation() {
    if (revoked) return;
    operation?.abort();
    operation = undefined;
    retireActive();
    detach(true);
    for (const surface of surfaces) surface.dispose();
    surfaces.clear();
    // Cleanup cannot retain permission to act or delay a fresh conversation.
    publish({
      threadId: null,
      native: undefined,
      messages: Object.freeze([]),
      surfaces: Object.freeze([]),
      activeSurface: null,
      busy: false,
      activity: 'idle',
      canSubmit: true,
      error: null,
      outcome: null,
      viewGeneration: snapshot.viewGeneration + 1,
    });
  }
  function revoke() {
    if (revoked) return;
    revoked = true;
    operation?.abort();
    operation = undefined;
    for (const surface of surfaces) surface.revoke();
    active = undefined;
    detach(true);
    listeners.clear();
    snapshot = Object.freeze({
      ...snapshot,
      activeSurface: null,
      busy: false,
      canSubmit: false,
      activity: 'idle',
    });
  }
  function dispose(): Promise<void> {
    if (disposal) return disposal;
    revoke();
    for (const surface of surfaces) surface.dispose();
    surfaces.clear();
    disposal = Promise.all([...cleanups]).then(() => undefined);
    return disposal;
  }
  return Object.freeze({
    getSnapshot: () => snapshot,
    subscribe(notify: () => void) {
      if (revoked) return () => undefined;
      const registration = { notify };
      listeners.add(registration);
      return () => {
        listeners.delete(registration);
      };
    },
    submit: (text: string) => submit(text),
    stop,
    newConversation,
    revoke,
    dispose,
  });
}
export type A2uiApplication = ReturnType<typeof createA2uiApplication>;
terminal.ts
import type { Session } from '@threadplane/ag-ui';
// eslint-disable-next-line @nx/enforce-module-boundaries -- Private same-layout data leaf, copied into installed consumers.
import {
  closedFields,
  copyData,
  equalData,
  plainRecord,
} from '../../../../../libs/cockpit-a2ui-react/src/data.js';
 
export type NativeSnapshot = ReturnType<Session['getSnapshot']>;
export interface OrdinaryMessage {
  readonly id: string;
  readonly role: 'user' | 'assistant';
  readonly content: string;
}
export interface NativeIdentity {
  readonly owner: object;
  readonly generation: number;
  readonly runId: string;
  readonly human: OrdinaryMessage;
  readonly previous: readonly OrdinaryMessage[];
}
const nonempty = (value: unknown): value is string =>
  typeof value === 'string' && value.length > 0;
function ordinary(input: unknown): input is readonly OrdinaryMessage[] {
  if (!Array.isArray(input)) return false;
  const ids = new Set<string>();
  return input.every((row) => {
    if (
      !closedFields(row, [
        'id',
        'role',
        'content',
        'name',
        'toolCalls',
        'subagentRunId',
      ]) ||
      !plainRecord(row) ||
      !nonempty(row.id) ||
      ids.has(row.id) ||
      !['user', 'assistant'].includes(String(row.role)) ||
      typeof row.content !== 'string' ||
      (row.name !== undefined && typeof row.name !== 'string') ||
      (row.subagentRunId !== undefined && row.subagentRunId !== null) ||
      (row.toolCalls !== undefined &&
        (!Array.isArray(row.toolCalls) || row.toolCalls.length !== 0))
    )
      return false;
    ids.add(row.id);
    return true;
  });
}
 
/** Binds only the initial native publication; terminal success cannot invent identity. */
export function createNativeObservation(input: {
  readonly owner: object;
  readonly generation: number;
  readonly threadId: string;
  readonly message: string;
  readonly previous: unknown;
  readonly usedRunIds: readonly string[];
  readonly valid?: () => boolean;
}) {
  const { owner, generation, threadId, message } = input;
  const valid = input.valid ?? (() => true);
  let identity: NativeIdentity | undefined,
    invalid = false;
  let previous: readonly OrdinaryMessage[] = [],
    usedRunIds: readonly string[] = [];
  try {
    const captured = copyData(input.previous),
      ids = copyData(input.usedRunIds);
    if (
      !valid() ||
      !ordinary(captured) ||
      !Array.isArray(ids) ||
      !ids.every(nonempty) ||
      !nonempty(threadId) ||
      typeof message !== 'string' ||
      !Number.isSafeInteger(generation) ||
      generation < 1
    )
      invalid = true;
    else {
      previous = captured;
      usedRunIds = ids as readonly string[];
    }
  } catch {
    invalid = true;
  }
  function read(value: unknown) {
    if (invalid || !valid()) return;
    const state = copyData(value);
    if (
      !valid() ||
      !plainRecord(state) ||
      !Array.isArray(state.transcript) ||
      !Array.isArray(state.subagents) ||
      state.subagents.length ||
      state.decision ||
      state.error ||
      !['running', 'idle'].includes(String(state.status)) ||
      !plainRecord(state.run) ||
      !nonempty(state.run.id) ||
      state.run.legacyInterrupt ||
      (state.run.outcome !== undefined && state.run.outcome !== 'success')
    )
      return;
    return { state, run: state.run, transcript: state.transcript };
  }
  function matches(rows: readonly unknown[], captured: NativeIdentity) {
    return (
      rows.length >= previous.length + 1 &&
      equalData(rows.slice(0, previous.length), previous) &&
      equalData(rows[previous.length], captured.human) &&
      valid()
    );
  }
  return Object.freeze({
    observe(snapshot: unknown) {
      if (invalid || !valid()) return;
      try {
        const readout = read(snapshot);
        if (!readout) {
          invalid = true;
          return;
        }
        const { state, run, transcript } = readout;
        if (identity) {
          if (run.id !== identity.runId || !matches(transcript, identity))
            invalid = true;
          return;
        }
        const user = transcript[previous.length];
        if (
          state.status !== 'running' ||
          run.outcome !== undefined ||
          run.terminal !== undefined ||
          usedRunIds.includes(run.id as string) ||
          transcript.length !== previous.length + 1 ||
          !ordinary(transcript) ||
          !equalData(transcript.slice(0, previous.length), previous) ||
          !plainRecord(user) ||
          user.role !== 'user' ||
          user.content !== message ||
          !valid()
        ) {
          invalid = true;
          return;
        }
        identity = Object.freeze({
          owner,
          generation,
          runId: run.id as string,
          human: user as unknown as OrdinaryMessage,
          previous,
        });
      } catch {
        invalid = true;
      }
    },
    identity: () => (!invalid && valid() ? identity : undefined),
    confirm(
      outcome: unknown,
      snapshot: unknown
    ): readonly OrdinaryMessage[] | undefined {
      try {
        if (outcome !== 'success' || !identity) return;
        const readout = read(snapshot);
        if (!readout) return;
        const { state, run, transcript } = readout,
          terminal = run.terminal;
        if (
          state.status !== 'idle' ||
          run.id !== identity.runId ||
          run.outcome !== 'success' ||
          !plainRecord(terminal) ||
          terminal.type !== 'RUN_FINISHED' ||
          terminal.threadId !== threadId ||
          terminal.runId !== identity.runId ||
          !ordinary(transcript) ||
          transcript.length !== previous.length + 2 ||
          !matches(transcript, identity)
        )
          return;
        if (
          terminal.outcome !== undefined &&
          (!closedFields(terminal.outcome, ['type', 'pendingToolCallIds']) ||
            !plainRecord(terminal.outcome) ||
            terminal.outcome.type !== 'success' ||
            (terminal.outcome.pendingToolCallIds !== undefined &&
              (!Array.isArray(terminal.outcome.pendingToolCallIds) ||
                terminal.outcome.pendingToolCallIds.length)))
        )
          return;
        const last = transcript.at(-1);
        return last?.role === 'assistant' && last.content.trim() && valid()
          ? transcript
          : undefined;
      } catch {
        return;
      }
    },
  });
}
export type NativeObservation = ReturnType<typeof createNativeObservation>;
protocol.ts
import {
  A2UI_BASIC_CATALOG_ID,
  createA2uiMessageParser,
} from '@threadplane/a2ui';
import { createA2uiSurfaces } from '@threadplane/content/a2ui';
import type { A2uiSurfacesSnapshot } from '@threadplane/content/a2ui';
import type { RenderSpecData } from '@threadplane/react/render';
import { closedFields, copyData, plainRecord, type Data } from './data.js';
 
export const AIRPORTS = Object.freeze([
  'LAX',
  'JFK',
  'SFO',
  'ORD',
  'BOS',
  'ATL',
  'DFW',
  'SEA',
  'MIA',
  'DEN',
]);
export const FARES = Object.freeze(['Economy', 'Premium', 'Business', 'First']);
export const BOOKING_FIELDS = Object.freeze([
  'origin',
  'dest',
  'date',
  'passengers',
  'fare_class',
] as const);
export type BookingField = (typeof BOOKING_FIELDS)[number];
export type SurfaceKind = 'booking' | 'results' | 'confirmation';
export type Component = Readonly<Record<string, Data>> & {
  readonly id: string;
  readonly component: string;
};
export interface PreparedSurface {
  readonly identity: object;
  readonly surfaceId: SurfaceKind;
  readonly prose: string;
  readonly rawSnapshot: A2uiSurfacesSnapshot;
  readonly model: Readonly<Record<string, Data>>;
  readonly components: Readonly<Record<string, Component>>;
  readonly spec: RenderSpecData;
  readonly sendDataModel: boolean;
  token(id: string): object | undefined;
  current(id: string, token: object | undefined): boolean;
  retireComponent(id: string): void;
  dispose(): void;
}
function requireThat(value: unknown): asserts value {
  if (!value) throw Error('Unsupported A2UI response');
}
function fields(
  value: unknown,
  allowed: readonly string[]
): asserts value is Record<string, Data> {
  requireThat(closedFields(value, allowed));
}
function id(value: unknown): asserts value is string {
  requireThat(
    typeof value === 'string' && value.length > 0 && value.length <= 128
  );
}
function variant(value: Data, allowed: readonly string[]) {
  requireThat(
    value === undefined ||
      (typeof value === 'string' && allowed.includes(value))
  );
}
function path(value: Data): string {
  fields(value, ['path']);
  requireThat(typeof value.path === 'string');
  return value.path;
}
function read(model: Readonly<Record<string, Data>>, p: string): Data {
  requireThat(
    /^\/[A-Za-z_][A-Za-z_0-9]*$/.test(p) && Object.hasOwn(model, p.slice(1))
  );
  return model[p.slice(1)];
}
function text(value: Data, model: Readonly<Record<string, Data>>) {
  if (typeof value !== 'string')
    requireThat(typeof read(model, path(value)) === 'string');
}
function choice(value: Data, allowed: readonly string[]) {
  requireThat(
    Array.isArray(value) &&
      value.length <= 1 &&
      value.every((v) => typeof v === 'string' && allowed.includes(v))
  );
}
export function assertBookingModel(
  model: unknown
): asserts model is Readonly<Record<string, Data>> {
  fields(model, BOOKING_FIELDS);
  requireThat(Object.keys(model).length === 5);
  choice(model.origin, AIRPORTS);
  choice(model.dest, AIRPORTS);
  choice(model.fare_class, FARES);
  requireThat(
    typeof model.date === 'string' &&
      Number.isSafeInteger(model.passengers) &&
      (model.passengers as number) > 0
  );
}
function contextValue(
  value: Data,
  key: string,
  model: Readonly<Record<string, Data>>
) {
  if (plainRecord(value)) {
    const p = path(value);
    requireThat(p === '/' + key);
    return read(model, p);
  }
  return value;
}
/** Shared protocol action admission; never evaluates functions. */
export function validateAction(
  action: Data,
  kind: SurfaceKind,
  model: Readonly<Record<string, Data>>
) {
  fields(action, ['event']);
  fields(action.event, ['name', 'context']);
  const event = action.event;
  fields(
    event.context,
    event.name === 'bookingSubmit'
      ? ['formId', ...BOOKING_FIELDS]
      : event.name === 'flightSelect'
      ? ['flightId']
      : ['formId']
  );
  const ctx = event.context;
  if (event.name === 'bookingSubmit') {
    requireThat(
      kind === 'booking' &&
        Object.keys(ctx).length === 6 &&
        ctx.formId === 'booking'
    );
    const values: Record<string, Data> = {};
    for (const key of BOOKING_FIELDS)
      values[key] = contextValue(ctx[key], key, model);
    assertBookingModel(values);
  } else if (event.name === 'flightSelect') {
    requireThat(kind === 'results' && Object.keys(ctx).length === 1);
    id(contextValue(ctx.flightId, 'flightId', model));
  } else
    requireThat(
      event.name === 'modifySearch' &&
        kind !== 'booking' &&
        Object.keys(ctx).length === 1 &&
        ctx.formId === 'booking'
    );
}
function validateComponent(
  c: Component,
  kind: SurfaceKind,
  model: Readonly<Record<string, Data>>
) {
  id(c.id);
  requireThat(typeof c.component === 'string');
  const props: Record<string, readonly string[]> = {
    Column: ['children'],
    Row: ['children'],
    List: ['children'],
    Card: ['child'],
    Text: ['text', 'variant'],
    Divider: ['axis'],
    Button: ['child', 'variant', 'action', 'checks'],
    ChoicePicker: ['label', 'options', 'value', 'variant', 'checks'],
    DateTimeInput: ['label', 'value', 'enableDate', 'enableTime', 'checks'],
    TextField: ['label', 'value', 'variant', 'checks'],
  };
  requireThat(Object.hasOwn(props, c.component));
  fields(c, ['id', 'component', 'catalogId', ...props[c.component]]);
  requireThat(
    c.catalogId === undefined || c.catalogId === A2UI_BASIC_CATALOG_ID
  );
  if (c.checks !== undefined)
    requireThat(Array.isArray(c.checks) && !c.checks.length);
  switch (c.component) {
    case 'Column':
    case 'Row':
    case 'List':
      requireThat(Array.isArray(c.children));
      for (const child of c.children) id(child);
      break;
    case 'Card':
      id(c.child);
      break;
    case 'Text':
      text(c.text, model);
      variant(c.variant, ['h1', 'h2', 'h3', 'h4', 'h5', 'body', 'caption']);
      break;
    case 'Divider':
      variant(c.axis, ['horizontal', 'vertical']);
      break;
    case 'Button':
      id(c.child);
      variant(c.variant, ['default', 'primary', 'borderless']);
      validateAction(c.action, kind, model);
      break;
    case 'ChoicePicker': {
      requireThat(kind === 'booking');
      text(c.label, model);
      variant(c.variant, ['mutuallyExclusive']);
      const p = path(c.value);
      requireThat(['/origin', '/dest', '/fare_class'].includes(p));
      const allowed = p === '/fare_class' ? FARES : AIRPORTS;
      choice(read(model, p), allowed);
      requireThat(
        Array.isArray(c.options) && c.options.length === allowed.length
      );
      const seen = new Set();
      for (const o of c.options) {
        fields(o, ['label', 'value']);
        requireThat(
          typeof o.label === 'string' &&
            typeof o.value === 'string' &&
            allowed.includes(o.value) &&
            !seen.has(o.value)
        );
        seen.add(o.value);
      }
      break;
    }
    case 'DateTimeInput':
      requireThat(
        kind === 'booking' &&
          path(c.value) === '/date' &&
          c.enableDate === true &&
          (c.enableTime === undefined || c.enableTime === false)
      );
      text(c.label, model);
      break;
    case 'TextField':
      requireThat(
        kind === 'booking' &&
          path(c.value) === '/passengers' &&
          c.variant === 'number'
      );
      text(c.label, model);
      break;
  }
}
function children(c: Component): readonly string[] {
  return Array.isArray(c.children)
    ? (c.children as readonly string[])
    : typeof c.child === 'string'
    ? [c.child]
    : [];
}
function validateTree(
  all: readonly Component[],
  kind: SurfaceKind,
  model: Readonly<Record<string, Data>>
) {
  requireThat(all.length > 0 && all.length <= 512);
  const map: Record<string, Component> = Object.create(null);
  for (const c of all) {
    fields(c, Object.keys(c));
    validateComponent(c, kind, model);
    requireThat(!Object.hasOwn(map, c.id));
    map[c.id] = c;
  }
  requireThat(Object.hasOwn(map, 'root') && map.root.component === 'Column');
  const visited = new Set<string>();
  function visit(key: string, depth: number) {
    requireThat(depth <= 64 && Object.hasOwn(map, key) && !visited.has(key));
    visited.add(key);
    const c = map[key];
    if (c.component === 'Button')
      requireThat(map[c.child as string]?.component === 'Text');
    for (const child of children(c)) visit(child, depth + 1);
  }
  visit('root', 0);
  requireThat(visited.size === all.length);
  return Object.freeze(map);
}
function project(map: Readonly<Record<string, Component>>): RenderSpecData {
  const elements: Record<string, unknown> = Object.create(null);
  for (const c of Object.values(map)) {
    const props: Record<string, Data> = Object.create(null);
    for (const [key, value] of Object.entries(c)) {
      if (
        [
          'id',
          'component',
          'catalogId',
          'children',
          'child',
          'action',
          'checks',
        ].includes(key)
      )
        continue;
      props[key] =
        plainRecord(value) && typeof value.path === 'string'
          ? { $bindState: value.path }
          : value;
    }
    if (['Row', 'Column', 'List'].includes(c.component))
      props.direction = c.component === 'Row' ? 'horizontal' : 'vertical';
    if (c.component === 'Divider') props.axis = c.axis ?? 'horizontal';
    if (c.component === 'ChoicePicker')
      props.variant = c.variant ?? 'mutuallyExclusive';
    if (c.component === 'Button') props.variant = c.variant ?? 'default';
    if (c.component === 'Text') props.variant = c.variant ?? 'body';
    elements[c.id] = { type: c.component, props, children: children(c) };
  }
  return copyData({ root: 'root', elements }) as unknown as RenderSpecData;
}
/** Complete canonical assistant payload only. Validate before parser and owner. */
export function prepareSurface(
  content: string,
  expected: SurfaceKind
): PreparedSurface {
  requireThat(['booking', 'results', 'confirmation'].includes(expected));
  const delimiter = '---a2ui_JSON---',
    at = content.indexOf(delimiter);
  requireThat(
    at >= 0 &&
      (at === 0 || content[at - 1] === '\n') &&
      content[at + delimiter.length] === '\n' &&
      content.indexOf(delimiter, at + delimiter.length) < 0
  );
  const payload = content.slice(at + delimiter.length + 1);
  requireThat(
    payload.endsWith('\n') &&
      new TextEncoder().encode(payload).length <= 1048576
  );
  const messages = payload
    .split('\n')
    .filter((line) => line.trim())
    .map((line) => JSON.parse(line));
  requireThat(messages.length === 2 || messages.length === 3);
  const commands = ['createSurface', 'updateComponents', 'updateDataModel'];
  for (let i = 0; i < messages.length; i++) {
    const m = messages[i];
    fields(m, ['version', commands[i]]);
    requireThat(m.version === 'v0.9' && Object.keys(m).length === 2);
    const command = m[commands[i]];
    fields(
      command,
      i === 0
        ? ['surfaceId', 'catalogId', 'sendDataModel']
        : i === 1
        ? ['surfaceId', 'components']
        : ['surfaceId', 'path', 'value']
    );
    requireThat(command.surfaceId === expected);
    id(command.surfaceId);
    if (i === 0)
      requireThat(
        command.catalogId === A2UI_BASIC_CATALOG_ID &&
          (command.sendDataModel === undefined ||
            typeof command.sendDataModel === 'boolean')
      );
    if (i === 1) requireThat(Array.isArray(command.components));
    if (i === 2)
      requireThat(
        (command.path === undefined || command.path === '/') &&
          plainRecord(command.value)
      );
  }
  const captured = copyData(messages) as unknown as typeof messages;
  const model =
    messages.length === 3
      ? captured[2].updateDataModel.value
      : Object.freeze({});
  if (expected === 'booking') assertBookingModel(model);
  validateTree(captured[1].updateComponents.components, expected, model);
  const owner = createA2uiSurfaces();
  try {
    const parsed = createA2uiMessageParser().push(payload);
    requireThat(parsed.length === messages.length);
    for (const message of parsed) owner.apply(message);
    const rawSnapshot = owner.getSnapshot();
    requireThat(rawSnapshot.surfaces.length === 1);
    const raw = rawSnapshot.surfaces[0];
    requireThat(raw.surfaceId === expected && plainRecord(raw.dataModel));
    const components = validateTree(
        raw.components as readonly Component[],
        expected,
        raw.dataModel
      ),
      spec = project(components);
    // Conservative aggregate capture includes duplicated retained representations.
    copyData({ captured, rawSnapshot, spec });
    let live = true;
    const tokens = new Map(
      Object.keys(components).map((id) => [id, Object.freeze({})])
    );
    return Object.freeze({
      identity: Object.freeze({}),
      surfaceId: expected,
      prose: content.slice(0, at),
      rawSnapshot,
      model: raw.dataModel,
      components,
      spec,
      sendDataModel: raw.sendDataModel === true,
      token: (id: string) => tokens.get(id),
      current: (id: string, token: object | undefined) =>
        live && token !== undefined && tokens.get(id) === token,
      retireComponent: (id: string) => {
        tokens.delete(id);
      },
      dispose: () => {
        if (live) {
          live = false;
          tokens.clear();
          owner.dispose();
        }
      },
    });
  } catch (error) {
    owner.dispose();
    throw error;
  }
}
form.ts
import type { RenderState } from '@threadplane/content/render-state';
import { equalData, plainRecord } from './data.js';
import {
  AIRPORTS,
  FARES,
  assertBookingModel,
  type BookingField,
  type PreparedSurface,
} from './protocol.js';
 
export interface BookingData {
  readonly origin: readonly string[];
  readonly dest: readonly string[];
  readonly fare_class: readonly string[];
  readonly date: string;
  readonly passengers: number;
}
export interface BookingSnapshot {
  readonly model: BookingData;
  readonly drafts: Readonly<Record<BookingField, string>>;
  readonly errors: Readonly<Partial<Record<BookingField, string>>>;
  readonly valid: boolean;
}
export interface BookingForm {
  readonly identity: object;
  getSnapshot(): BookingSnapshot;
  subscribe(notify: () => void): () => void;
  edit(componentId: string, draft: string): void;
  captureEdit(componentId: string): (draft: string) => void;
  canSubmit(): boolean;
  dispose(): void;
}
/** Calendar validation uses only lexical integers, never local time or today's date. */
function calendar(value: string) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
  const [year, month, day] = value.split('-').map(Number);
  const days = [
    31,
    year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
    31,
    30,
    31,
    30,
    31,
    31,
    30,
    31,
    30,
    31,
  ];
  return (
    year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1]
  );
}
function passenger(value: string) {
  return (
    /^\d+$/.test(value) &&
    Number.isSafeInteger(Number(value)) &&
    Number(value) > 0
  );
}
function errorsFor(
  model: BookingData,
  drafts: Readonly<Record<BookingField, string>>
) {
  const errors: Partial<Record<BookingField, string>> = {};
  if (!AIRPORTS.includes(drafts.origin))
    errors.origin = 'Choose an origin airport.';
  if (!AIRPORTS.includes(drafts.dest))
    errors.dest = 'Choose a destination airport.';
  else if (drafts.origin === drafts.dest)
    errors.dest = 'Choose different origin and destination airports.';
  if (!FARES.includes(drafts.fare_class))
    errors.fare_class = 'Choose a fare class.';
  if (!calendar(drafts.date)) errors.date = 'Enter a real date as YYYY-MM-DD.';
  if (!passenger(drafts.passengers))
    errors.passengers = 'Enter a positive safe whole number.';
  // Each valid draft must describe exactly the same typed authority.
  if (!errors.passengers && Number(drafts.passengers) !== model.passengers)
    errors.passengers = 'Passenger value changed outside this form.';
  for (const field of ['origin', 'dest', 'fare_class'] as const)
    if (
      !errors[field] &&
      (model[field].length !== 1 || model[field][0] !== drafts[field])
    )
      errors[field] = 'Selection changed outside this form.';
  if (!errors.date && drafts.date !== model.date)
    errors.date = 'Date changed outside this form.';
  return Object.freeze(errors);
}
/** Borrow one application-owned render state. This facade alone publishes validity. */
export function createBookingForm(
  surface: PreparedSurface,
  state: RenderState
): BookingForm {
  if (
    surface.surfaceId !== 'booking' ||
    !equalData(surface.model, state.getSnapshot())
  )
    throw Error('Booking owner mismatch');
  assertBookingModel(state.getSnapshot());
  const initial = state.getSnapshot() as unknown as BookingData;
  const controls = new Map<string, { field: BookingField; token: object }>();
  for (const c of Object.values(surface.components)) {
    if (
      !['ChoicePicker', 'DateTimeInput', 'TextField'].includes(c.component) ||
      !plainRecord(c.value)
    )
      continue;
    const field = String(c.value.path).slice(1) as BookingField;
    controls.set(c.id, { field, token: surface.token(c.id)! });
  }
  const root = surface.token('root');
  let live = true,
    busy = false,
    writing = false,
    drift = false;
  const queue: Array<{ id: string; token: object | undefined; draft: string }> =
    [];
  const registrations = new Set<{ notify: () => void }>();
  function snapshot(
    model: BookingData,
    drafts: Readonly<Record<BookingField, string>>
  ): BookingSnapshot {
    const errors = drift
      ? Object.freeze({
          ...errorsFor(model, drafts),
          passengers:
            'Form data changed outside this form. Start a new conversation.',
        })
      : errorsFor(model, drafts);
    return Object.freeze({
      model,
      drafts: Object.freeze(drafts),
      errors,
      valid: !drift && Object.keys(errors).length === 0,
    });
  }
  let current = snapshot(initial, {
    origin: initial.origin[0] ?? '',
    dest: initial.dest[0] ?? '',
    fare_class: initial.fare_class[0] ?? '',
    date: initial.date,
    passengers: String(initial.passengers),
  });
  function eligible() {
    return (
      live &&
      !drift &&
      surface.current('root', root) &&
      [...controls].every(([id, c]) => surface.current(id, c.token))
    );
  }
  function publish() {
    for (const r of [...registrations]) {
      if (!live) break;
      if (registrations.has(r))
        try {
          r.notify();
        } catch {
          /* An observer cannot reject a command. */
        }
    }
  }
  const unsubscribe = state.subscribe(() => {
    if (!live || writing) return;
    // An independent writer violates the borrowed form contract. Retire authority.
    drift = true;
    current = snapshot(current.model, current.drafts);
    const wasBusy = busy;
    busy = true;
    try {
      publish();
    } finally {
      busy = wasBusy;
    }
  });
  function run(id: string, token: object | undefined, draft: string) {
    if (!eligible() || typeof draft !== 'string') return;
    queue.push({ id, token, draft });
    if (busy) return;
    busy = true;
    try {
      while (queue.length && eligible()) {
        const command = queue.shift()!,
          control = controls.get(command.id);
        if (
          !control ||
          control.token !== command.token ||
          !surface.current(command.id, command.token)
        )
          continue;
        const { field } = control,
          c = surface.components[command.id],
          value = command.draft;
        if (current.drafts[field] === value) continue;
        let accepted = false,
          typed: unknown;
        if (field === 'passengers') {
          accepted = passenger(value);
          typed = Number(value);
        } else if (field === 'date') {
          accepted = calendar(value);
          typed = value;
        } else {
          const allowed = field === 'fare_class' ? FARES : AIRPORTS;
          const options = Array.isArray(c.options) ? c.options : [];
          accepted =
            value === '' ||
            (allowed.includes(value) &&
              options.some((o) => plainRecord(o) && o.value === value));
          typed = value === '' ? [] : [value];
        }
        const drafts = Object.freeze({ ...current.drafts, [field]: value });
        if (accepted) {
          writing = true;
          try {
            state.set('/' + field, typed);
          } finally {
            writing = false;
          }
          if (
            !equalData(state.getSnapshot(), {
              ...current.model,
              [field]: typed,
            })
          ) {
            drift = true;
            current = snapshot(current.model, drafts);
            publish();
            break;
          }
        }
        if (!eligible()) break;
        current = snapshot(
          state.getSnapshot() as unknown as BookingData,
          drafts
        );
        publish();
      }
    } finally {
      queue.length = 0;
      busy = false;
    }
  }
  return Object.freeze({
    identity: surface.identity,
    getSnapshot: () => current,
    subscribe(notify: () => void) {
      if (!live) return () => undefined;
      const registration = { notify };
      registrations.add(registration);
      return () => {
        registrations.delete(registration);
      };
    },
    edit: (id: string, draft: string) =>
      run(id, controls.get(id)?.token, draft),
    captureEdit(id: string) {
      const token = controls.get(id)?.token;
      return (draft: string) => run(id, token, draft);
    },
    canSubmit: () =>
      !busy &&
      eligible() &&
      Object.is(state.getSnapshot(), current.model) &&
      current.valid,
    dispose() {
      if (live) {
        live = false;
        queue.length = 0;
        registrations.clear();
        unsubscribe();
      }
    },
  });
}
actions.ts
import type { A2uiActionMessage } from '@threadplane/a2ui';
import { copyData, equalData, plainRecord, type Data } from './data.js';
import { validateAction, type PreparedSurface } from './protocol.js';
import type { BookingForm } from './form.js';
 
export interface ActionOptions {
  readonly form?: BookingForm;
  readonly timestamp: () => string;
  readonly dispatch: (payload: Readonly<A2uiActionMessage>) => void;
}
export interface Actions {
  capability(componentId: string): () => boolean;
  dispose(): void;
}
/** Captures event data only; the application owns admission and transport. */
export function createActions(
  surface: PreparedSurface,
  options: ActionOptions
): Actions {
  const { form, timestamp: clock, dispatch } = options;
  if (form && form.identity !== surface.identity)
    throw Error('Action form epoch mismatch');
  if (surface.surfaceId === 'booking' && !form)
    throw Error('Booking action needs its form facade');
  let live = true,
    activating = false;
  return Object.freeze({
    capability(id: string) {
      const token = surface.token(id),
        component = surface.components[id];
      const labelId = String(component?.child),
        labelToken = surface.token(labelId);
      return () => {
        if (
          !live ||
          activating ||
          !surface.current(id, token) ||
          !surface.current(labelId, labelToken) ||
          component?.component !== 'Button'
        )
          return false;
        if (form && !form.canSubmit()) return false;
        const snapshot = form?.getSnapshot();
        const model = (snapshot?.model ?? surface.model) as unknown as Readonly<
          Record<string, Data>
        >;
        validateAction(component.action, surface.surfaceId, model);
        if (
          !plainRecord(component.action) ||
          !plainRecord(component.action.event) ||
          !plainRecord(component.action.event.context)
        )
          return false;
        const event = component.action.event,
          context: Record<string, Data> = {};
        const authoredContext = event.context as Readonly<Record<string, Data>>;
        for (const [key, value] of Object.entries(authoredContext))
          context[key] = plainRecord(value)
            ? model[String(value.path).slice(1)]
            : value;
        // A literal booking value cannot override a later visible edit.
        if (
          event.name === 'bookingSubmit' &&
          !equalData(
            { ...context, formId: undefined },
            { ...model, formId: undefined }
          )
        )
          return false;
        const child = surface.components[String(component.child)];
        const authored = child?.component === 'Text' ? child.text : undefined;
        const label = plainRecord(authored)
          ? model[String(authored.path).slice(1)]
          : authored;
        activating = true;
        try {
          const timestamp = clock();
          if (
            typeof timestamp !== 'string' ||
            !timestamp ||
            !Number.isFinite(Date.parse(timestamp))
          )
            throw Error('Invalid action timestamp');
          const payload = copyData({
            version: 'v0.9',
            action: {
              name: event.name,
              surfaceId: surface.surfaceId,
              sourceComponentId: id,
              timestamp,
              context,
              ...(typeof label === 'string' ? { label } : {}),
            },
            ...(surface.sendDataModel
              ? {
                  metadata: {
                    a2uiClientDataModel: {
                      surfaces: { [surface.surfaceId]: model },
                    },
                  },
                }
              : {}),
          }) as unknown as Readonly<A2uiActionMessage>;
          // Clock/capture callbacks cannot resurrect retired or changed authority.
          if (
            !live ||
            !surface.current(id, token) ||
            !surface.current(labelId, labelToken) ||
            (form && (!form.canSubmit() || form.getSnapshot() !== snapshot))
          )
            return false;
          dispatch(payload);
          return true;
        } finally {
          activating = false;
        }
      };
    },
    dispose() {
      live = false;
    },
  });
}
views.tsx
import {
  Children,
  createContext,
  useContext,
  useId,
  useRef,
  useState,
} from 'react';
import { useAgent } from '@threadplane/react';
import {
  RenderSpec,
  type ReactRenderRegistry,
  type RenderValue,
  type RenderViewProps,
} from '@threadplane/react/render';
import type { SurfaceView } from './surface';
import type { BookingField } from './protocol';
import type { BookingSnapshot } from './form';
 
const empty = Object.freeze({
  model: Object.freeze({}),
  drafts: Object.freeze({}),
  errors: Object.freeze({}),
  valid: true,
});
const absentForm = Object.freeze({
  getSnapshot: () => empty,
  subscribe: () => () => undefined,
});
interface Borrowed {
  readonly surface: SurfaceView;
  readonly form: BookingSnapshot | typeof empty;
  readonly disabled: boolean;
  readonly showErrors: boolean;
  readonly focusInvalid: () => void;
}
const Context = createContext<Borrowed | null>(null);
function useSurface() {
  const context = useContext(Context);
  if (!context) throw Error('Missing surface presentation');
  return context;
}
const text = (value: RenderValue) =>
  typeof value === 'string' || typeof value === 'number' ? String(value) : '';
function Column({ children }: RenderViewProps) {
  return <div className="a2ui-column">{children}</div>;
}
function Row({ children }: RenderViewProps) {
  return <div className="a2ui-row">{children}</div>;
}
function List({ children }: RenderViewProps) {
  return (
    <ul className="a2ui-list">
      {Children.map(children, (child) => (
        <li>{child}</li>
      ))}
    </ul>
  );
}
function Card({ children }: RenderViewProps) {
  return <section className="a2ui-card">{children}</section>;
}
function Text({ props }: RenderViewProps) {
  const value = text(props.text);
  switch (props.variant) {
    case 'h1':
      return <h1>{value}</h1>;
    case 'h2':
      return <h2>{value}</h2>;
    case 'h3':
      return <h3>{value}</h3>;
    case 'h4':
      return <h4>{value}</h4>;
    case 'h5':
      return <h5>{value}</h5>;
    case 'caption':
      return <small>{value}</small>;
    default:
      return <span>{value}</span>;
  }
}
function Divider({ props }: RenderViewProps) {
  return (
    <div
      role="separator"
      aria-orientation={props.axis === 'vertical' ? 'vertical' : 'horizontal'}
      className="a2ui-divider"
    />
  );
}
function Control({
  props,
  bindings,
  elementKey,
  kind,
}: RenderViewProps & { readonly kind: 'choice' | 'date' | 'number' }) {
  const { surface, form, disabled, showErrors } = useSurface();
  const field = (bindings?.value ?? '').slice(1) as BookingField;
  // Binding admission and field drafts belong to the application facade.
  const drafts = form.drafts as Readonly<Partial<Record<BookingField, string>>>;
  const errors = form.errors as Readonly<Partial<Record<BookingField, string>>>;
  const [touchedOwner, setTouchedOwner] = useState<object | null>(null);
  const owner = surface.form ?? surface;
  const initial = surface.model[field];
  const initialDraft = Array.isArray(initial)
    ? text(initial[0])
    : text(initial);
  const edited = touchedOwner === owner || drafts[field] !== initialDraft;
  const error = showErrors || edited ? errors[field] : undefined;
  const generated = useId();
  const id = generated + '-control',
    errorId = generated + '-error';
  const shared = {
    id,
    disabled,
    value: drafts[field] ?? '',
    'aria-invalid': !!error,
    'data-invalid': !!errors[field],
    'aria-describedby': error ? errorId : undefined,
    onChange: (event: { currentTarget: { value: string } }) => {
      if (!disabled) {
        setTouchedOwner(owner);
        surface.edits[elementKey]?.(event.currentTarget.value);
      }
    },
    onBlur: () => {
      if (!disabled) setTouchedOwner(owner);
    },
  };
  return (
    <div className="a2ui-field">
      <label htmlFor={id}>{text(props.label)}</label>
      {kind === 'choice' ? (
        <select {...shared}>
          <option value="">Choose…</option>
          {Array.isArray(props.options) &&
            props.options.map((option) => {
              const item = option as Readonly<Record<string, RenderValue>>;
              return (
                <option key={text(item.value)} value={text(item.value)}>
                  {text(item.label)}
                </option>
              );
            })}
        </select>
      ) : (
        <input
          {...shared}
          type={kind}
          {...(kind === 'number'
            ? { min: 1, step: 1, inputMode: 'numeric' as const }
            : {})}
        />
      )}
      {error && (
        <p id={errorId} role="alert" className="a2ui-error">
          {error}
        </p>
      )}
    </div>
  );
}
function ChoicePicker(props: RenderViewProps) {
  return <Control {...props} kind="choice" />;
}
function DateTimeInput(props: RenderViewProps) {
  return <Control {...props} kind="date" />;
}
function TextField(props: RenderViewProps) {
  return <Control {...props} kind="number" />;
}
function Button({ props, children, elementKey }: RenderViewProps) {
  const { surface, disabled, focusInvalid } = useSurface();
  return (
    <button
      type="button"
      disabled={disabled}
      className="a2ui-button"
      data-variant={text(props.variant)}
      onClick={() => {
        if (!disabled && !surface.actions[elementKey]?.()) focusInvalid();
      }}
    >
      {children}
    </button>
  );
}
/** Components stay stable across immutable model snapshots and React render replay. */
export const a2uiRegistry: ReactRenderRegistry = Object.freeze({
  Column,
  Row,
  List,
  Card,
  Text,
  Divider,
  ChoicePicker,
  DateTimeInput,
  TextField,
  Button,
});
/** A borrowed presentation lifetime never owns or disposes an application resource. */
export function A2uiSurface({
  surface,
  active,
}: {
  readonly surface: SurfaceView;
  readonly active: boolean;
}) {
  const form = useAgent<BookingSnapshot | typeof empty>(
    surface.form ?? absentForm
  );
  const container = useRef<HTMLElement>(null);
  const [errorsOwner, setErrorsOwner] = useState<object | null>(null);
  const owner = surface.form ?? surface;
  return (
    <section
      ref={container}
      className="a2ui-surface"
      aria-label={active ? 'Current flight preview' : 'Earlier flight preview'}
    >
      {!active && <p className="a2ui-history">Earlier response · read only</p>}
      {surface.prose && <p className="a2ui-prose">{surface.prose}</p>}
      <Context.Provider
        value={{
          surface,
          form,
          disabled: !active,
          showErrors: errorsOwner === owner,
          focusInvalid: () => {
            setErrorsOwner(owner);
            container.current
              ?.querySelector<HTMLElement>('[data-invalid="true"]')
              ?.focus();
          },
        }}
      >
        <RenderSpec
          spec={surface.spec}
          registry={a2uiRegistry}
          state={surface.form ? form.model : surface.model}
        />
      </Context.Provider>
      <details className="a2ui-wire">
        <summary>Response details</summary>
        <pre>{surface.wire}</pre>
      </details>
    </section>
  );
}

Stop and recover

Stop or an uncertain run requires New conversation before continuing. Late responses from the old conversation cannot restore its controls. Correct ordinary form-validation errors in the current form. Modify restores the values supplied by the server. The closed flight profile supports ten views; it does not claim full A2UI catalog parity or SSR support.

graph.py
"""
A2UI Aviation Booking Form Graph
 
LLM-authored A2UI surfaces:
- build_form: emits a flight-booking form via gpt-5 with structured output
- search_flights: post-submit, calls find_routes() and emits a results surface
 
Both surfaces are constrained by Pydantic schemas (A2uiComponent +
BookingFormSpec / FlightResultsSpec) and validated; on ValidationError,
the LLM is re-prompted with the error up to 2 retries. After 3 total
attempts, a hardcoded sentinel form is emitted so the demo doesn't 500.
 
This replaces the prior hardcoded contact-form implementation. The
prior file's claim that "LLMs cannot reliably emit A2UI JSONL" is
disproven by schema-constrained structured output — the LLM authors
the components list, code wraps it in the deterministic envelope keys.
"""
 
import json
import logging
import os
import re
from typing import Any, Literal
 
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, MessagesState, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command
from langgraph_sdk import get_client
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
 
 
# Inlined flight fixtures - standalone has no aviation_data module.
_FLIGHTS = [
    {"flight_number": "UA123", "airline": "UA", "from": "LAX", "to": "JFK",
     "depart_local": "08:00", "arrive_local": "16:30", "duration_min": 330,
     "status": "on_time", "gate": "B14", "aircraft": "Boeing 787"},
    {"flight_number": "AA456", "airline": "AA", "from": "JFK", "to": "LAX",
     "depart_local": "10:00", "arrive_local": "13:30", "duration_min": 390,
     "status": "on_time", "gate": "T5-22", "aircraft": "Boeing 777"},
    {"flight_number": "DL789", "airline": "DL", "from": "ATL", "to": "ORD",
     "depart_local": "07:15", "arrive_local": "08:45", "duration_min": 150,
     "status": "delayed", "gate": "A12", "aircraft": "Airbus A320"},
    {"flight_number": "B6101", "airline": "B6", "from": "BOS", "to": "MIA",
     "depart_local": "06:30", "arrive_local": "10:15", "duration_min": 225,
     "status": "on_time", "gate": "C8", "aircraft": "Airbus A321"},
    {"flight_number": "UA204", "airline": "UA", "from": "SFO", "to": "SEA",
     "depart_local": "09:00", "arrive_local": "11:00", "duration_min": 120,
     "status": "on_time", "gate": "F11", "aircraft": "Boeing 737"},
]
 
 
class _AsyncFn:
    def __init__(self, fn):
        self._fn = fn
 
    async def ainvoke(self, args):
        return self._fn(**args)
 
 
def _find_routes_impl(from_code, to_code, date_offset_days=0):
    return [f for f in _FLIGHTS if f["from"] == from_code and f["to"] == to_code]
 
 
find_routes = _AsyncFn(_find_routes_impl)
 
 
def _lookup_flight_impl(flight_number: str) -> dict | None:
    """Inline mirror of aviation_tools.lookup_flight for the standalone."""
    return next((f for f in _FLIGHTS if f["flight_number"] == flight_number), None)
 
 
lookup_flight = _AsyncFn(_lookup_flight_impl)
 
_logger = logging.getLogger(__name__)
 
A2UI_PREFIX = "---a2ui_JSON---"
 
# 10 IATA airports from aviation_data.py
AIRPORT_CODES = ["LAX", "JFK", "SFO", "ORD", "BOS", "ATL", "DFW", "SEA", "MIA", "DEN"]
FARE_CLASSES = ["Economy", "Premium", "Business", "First"]
 
# Catalog component names — A2UI v0.9 basic catalog
# (https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json).
# `component` must be one of these names; an unknown name renders nothing
# visible — silent failure mode, hence the field_validator below.
ALLOWED_COMPONENTS = frozenset({
    "AudioPlayer", "Button", "Card", "CheckBox", "ChoicePicker", "Column",
    "DateTimeInput", "Divider", "Icon", "Image", "List", "Modal", "Row",
    "Slider", "Tabs", "Text", "TextField", "Video",
})
 
 
# ── Pydantic schemas ────────────────────────────────────────────────────────
 
# region component-schema
class A2uiComponent(BaseModel):
    """Single A2UI v0.9 updateComponents entry.
 
    Components are FLAT — the catalog component name is the `component`
    string field and the per-component props sit at the top level of the
    same object:
        {id: "name_field", component: "TextField",
         label: "Name", value: {path: "/name"}}
 
    Dynamic values are bare literals or {"path": "/json/pointer"} bindings.
    Children are plain id arrays (or {path, componentId} templates).
    Actions are {"event": {"name": "...", "context": {...}}} where context
    is a plain object.
 
    Key per-component notes the LLM must respect:
      Card({child: "<id>"})                        — single child only
      Button({child: "<text-id>", action: {...}})  — child is a Text id (label)
      Column/Row/List({children: ["id1", "id2"]})
      TextField({label, value: {path:"/p"}, variant: "shortText"|"number"|...})
      ChoicePicker({label, options:[{label,value}], value:{path}, variant:"mutuallyExclusive"})
      DateTimeInput({label, value:{path:"/p"}, enableDate: true})
      Text({text: "literal or {path:'/p'}", variant?: "h1"|"h2"|"body"|...})
      Divider({})
    """
    model_config = {"extra": "allow"}
 
    id: str
    component: str = Field(
        description=(
            "Catalog component name. Must be one of: "
            + ", ".join(sorted(ALLOWED_COMPONENTS))
        ),
    )
 
    @field_validator("component")
    @classmethod
    def _known_component(cls, v: str) -> str:
        if v not in ALLOWED_COMPONENTS:
            raise ValueError(
                f"component '{v}' not in catalog. Allowed: {sorted(ALLOWED_COMPONENTS)}"
            )
        return v
# endregion
 
 
# region surface-spec
class _SurfaceSpec(BaseModel):
    """Common shape — both booking and results surfaces produce the same
    triple (surface_id, data_model, components)."""
    surface_id: str = Field(description="Surface id. Use 'booking' for the form, 'results' for flights.")
    data_model: dict[str, Any] = Field(description="Initial form/state values, e.g. prefills.")
    components: list[A2uiComponent]
 
 
class BookingFormSpec(_SurfaceSpec):
    @model_validator(mode="after")
    def _booking_picker_domains(self) -> "BookingFormSpec":
        for component_id, path, domain in (
            ("origin", "/origin", AIRPORT_CODES),
            ("dest", "/dest", AIRPORT_CODES),
            ("fare", "/fare_class", FARE_CLASSES),
        ):
            # Count both the intended id and any picker claiming its binding.
            # Inspect extras before comparing domains: malformed values may be
            # dictionaries/lists, and must raise ValidationError, not TypeError.
            matches = []
            for component in self.components:
                binding = getattr(component, "value", None)
                if component.id == component_id or (
                    component.component == "ChoicePicker"
                    and isinstance(binding, dict)
                    and binding.get("path") == path
                ):
                    matches.append(component)
            if len(matches) != 1:
                raise ValueError(
                    f"Booking picker '{component_id}' must occur exactly once "
                    f"as ChoicePicker bound to {path}"
                )
            component = matches[0]
            binding = getattr(component, "value", None)
            if (
                component.id != component_id
                or component.component != "ChoicePicker"
                or not isinstance(binding, dict)
                or binding.get("path") != path
            ):
                raise ValueError(
                    f"Booking picker '{component_id}' must be a ChoicePicker "
                    f"with value={{'path': '{path}'}}"
                )
            options = getattr(component, "options", None)
            if not isinstance(options, list) or any(
                not isinstance(option, dict)
                or not isinstance(option.get("label"), str)
                or not isinstance(option.get("value"), str)
                for option in options
            ):
                raise ValueError(
                    f"Booking picker '{component_id}' options must be a list "
                    "of objects with string labels and values"
                )
            values = [option["value"] for option in options]
            if len(values) != len(domain) or set(values) != set(domain):
                raise ValueError(
                    f"Booking picker '{component_id}' must contain each value "
                    f"exactly once from {domain}; got {values}"
                )
        return self
 
 
class FlightResultsSpec(_SurfaceSpec):
    pass
 
 
class ConfirmationSpec(_SurfaceSpec):
    """Booking confirmation surface — selected flight + prior party context."""
    pass
# endregion
 
 
# ── Envelope wrapping ───────────────────────────────────────────────────────
 
# region envelope-wrapping
# A2UI v0.9 wire format (a2ui.org server_to_client.json): every envelope
# carries "version": "v0.9". Order matters: createSurface first (surfaceId +
# catalogId), then updateComponents (flat components; exactly one has id
# "root", which is the root of the tree), then updateDataModel (path + value)
# as needed. There is no beginRendering envelope in v0.9.
 
CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"
 
 
def _wrap_envelopes(spec: _SurfaceSpec) -> str:
    """Wrap a validated SurfaceSpec into A2UI v0.9 JSONL."""
    lines = [
        json.dumps({"version": "v0.9", "createSurface": {
            "surfaceId": spec.surface_id,
            "catalogId": CATALOG_ID,
        }}),
        json.dumps({"version": "v0.9", "updateComponents": {
            "surfaceId": spec.surface_id,
            "components": [c.model_dump(exclude_none=True) for c in spec.components],
        }}),
    ]
    if spec.data_model:
        lines.append(json.dumps({"version": "v0.9", "updateDataModel": {
            "surfaceId": spec.surface_id,
            "path": "/",
            "value": spec.data_model,
        }}))
    return A2UI_PREFIX + "\n" + "\n".join(lines) + "\n"
# endregion
 
 
# ── LLM + retry ─────────────────────────────────────────────────────────────
 
# gpt-5 with low reasoning effort: PR #372 established gpt-5 follows
# directive precisely; "low" gives slightly more headroom than "minimal"
# for schema compliance.
_llm: ChatOpenAI | None = None
 
 
def _get_llm() -> ChatOpenAI:
    """Lazy-initialize the LLM so imports succeed without OPENAI_API_KEY set."""
    global _llm
    if _llm is None:
        _llm = ChatOpenAI(
            model="gpt-5",
            streaming=True,
            reasoning_effort="low",
        )
    return _llm
 
 
async def _emit_with_retry(
    spec_cls: type[_SurfaceSpec],
    base_messages: list[Any],
    max_attempts: int = 3,
) -> _SurfaceSpec:
    """Call the LLM with structured output, retrying on validation failure.
 
    Each retry re-injects the error message so the model has a chance
    to correct its output. After max_attempts, raises RuntimeError.
    """
    # method="function_calling" is required because our schema uses
    # `dict[str, Any]` fields (value/selected/checked/checks/action) for
    # A2UI binding payloads. OpenAI's default strict structured-output mode
    # demands additionalProperties=false on every nested object and rejects
    # open dicts. Function-calling mode is more flexible and the model
    # still adheres to the rest of the schema (especially the Literal[...]
    # on component type, which is the actual safety gate we need).
    llm = _get_llm().with_structured_output(spec_cls, method="function_calling")
    messages = list(base_messages)
    last_err: Exception | None = None
    for attempt in range(max_attempts):
        try:
            return await llm.ainvoke(messages)
        except ValidationError as err:
            last_err = err
            _logger.warning(
                "A2UI structured-output validation failed (attempt %d/%d): %s",
                attempt + 1, max_attempts, err,
            )
            messages = list(base_messages) + [
                AIMessage(content=(
                    f"Previous attempt failed schema validation: {err}. "
                    "Try again, strictly matching the schema. "
                    "Do not invent component types outside the Literal list."
                )),
            ]
    raise RuntimeError(
        f"LLM failed structured output after {max_attempts} attempts: {last_err}"
    )
 
 
# ── build_form node ─────────────────────────────────────────────────────────
 
_AIRPORT_OPTIONS = [{"label": c, "value": c} for c in AIRPORT_CODES]
_FARE_OPTIONS = [{"label": c, "value": c} for c in FARE_CLASSES]
 
# The form's data_model carries the field default values. On first turn this
# is blank; on a "Modify search" turn (after Select → confirmation surface →
# Modify search) the user expects to see their prior origin/dest/date/
# passengers/fare_class already populated. `build_form` walks message history
# via `_extract_prior_submit_context` and substitutes those values into
# {data_model_json} below.
# ChoicePicker values bind to string ARRAYS in the data model (v0.9
# DynamicStringList), so the picker-backed keys hold lists; the server
# scalarizes them back to plain strings on action receipt (_scalarize).
_BLANK_FORM_DEFAULTS: dict[str, Any] = {
    "origin": [], "dest": [], "date": "", "passengers": 1, "fare_class": ["Economy"],
}
 
# Keys rendered by a ChoicePicker — their data_model values are string arrays.
_CHOICE_KEYS = ("origin", "dest", "fare_class")
 
# `__DATA_MODEL_DEFAULTS__` is a non-brace sentinel substituted at call-time
# by `build_form()` via `str.replace()` — using `.format()` would conflict
# with the many literal-brace JSON examples below.
_BUILD_FORM_SYSTEM_TMPL = f"""You are an aviation booking-form designer. Emit an A2UI v0.9 booking form using the structured output schema.
 
A2UI FORMAT (CRITICAL): each component is FLAT — `{{"id": "...", "component": "<ComponentName>", <props...>}}`. The component name is a plain string and the props sit at the top level of the same object. ComponentName must be one of:
  Column, Row, Card, Text, TextField, ChoicePicker, DateTimeInput, CheckBox, Button, Divider, List, Image, Icon, Modal, Slider, Tabs
 
Dynamic values are bare literals ("LAX", 3, true) or data bindings `{{"path": "/json/pointer"}}`. Never wrap literals.
 
Per-component shapes:
  Column / Row / List: {{"children": ["id1", "id2"]}}
  Card:                {{"child": "<id>"}}            ← single child only
  Button:              {{"child": "<text-id>", "variant": "primary", "action": {{"event": {{"name": "<eventName>", "context": {{"formId": "booking"}}}}}}}}
  Text:                {{"text": "literal string", "variant": "h2"}}    (h1/h2/h3/h4/h5/caption/body)
  TextField:           {{"label": "Field", "value": {{"path": "/p"}}, "variant": "shortText"}}  (shortText/longText/number/obscured)
  DateTimeInput:       {{"label": "Date", "value": {{"path": "/p"}}, "enableDate": true}}
  ChoicePicker:        {{"label": "Origin", "options": [{{"label":"LAX","value":"LAX"}}], "value": {{"path":"/origin"}}, "variant": "mutuallyExclusive"}}
  CheckBox:            {{"label": "...", "value": {{"path":"/p"}}}}
  Divider:             {{}}
 
Exactly ONE component MUST have id "root" — it is the root of the tree.
 
Required form composition for THIS task:
  surface_id MUST be "booking"
  data_model MUST be __DATA_MODEL_DEFAULTS__  ← use these values verbatim as the field defaults
 
  Build this component tree:
    root (Column, children=["card"])
    card (Card, child=card_col)
    card_col (Column, children=[title, origin, dest, date, passengers, fare, submit])
    title (Text, text="Book a flight", variant="h2")
    origin (ChoicePicker, label="Origin", options={_AIRPORT_OPTIONS}, value={{"path":"/origin"}}, variant="mutuallyExclusive")
    dest (ChoicePicker, label="Destination", options={_AIRPORT_OPTIONS}, value={{"path":"/dest"}}, variant="mutuallyExclusive")
    date (DateTimeInput, label="Departure date (YYYY-MM-DD)", value={{"path":"/date"}}, enableDate=true)
    passengers (TextField, label="Passengers", value={{"path":"/passengers"}}, variant="number")
    fare (ChoicePicker, label="Fare class", options={_FARE_OPTIONS}, value={{"path":"/fare_class"}}, variant="mutuallyExclusive")
    submit (Button, child=submit_label, variant="primary", action={{"event":{{"name":"bookingSubmit","context":{{
                                                                  "formId":"booking",
                                                                  "origin":{{"path":"/origin"}},
                                                                  "dest":{{"path":"/dest"}},
                                                                  "date":{{"path":"/date"}},
                                                                  "passengers":{{"path":"/passengers"}},
                                                                  "fare_class":{{"path":"/fare_class"}}
                                                                }}}}}})
    submit_label (Text, text="Search flights")
 
Use these exact ids."""
 
 
def _comp(id_: str, name: str, props: dict[str, Any]) -> A2uiComponent:
    """Tiny helper so the sentinels read naturally."""
    return A2uiComponent(id=id_, component=name, **props)
 
 
def _form_defaults_from_prior(prior: dict[str, Any]) -> dict[str, Any]:
    """Project prior bookingSubmit context onto the form's data_model schema.
    Falls back to blanks for any missing key so the returned dict always has
    the full {origin, dest, date, passengers, fare_class} shape. Prior context
    values are scalars; ChoicePicker-backed keys are re-wrapped into the
    string-array shape their data-model binding expects."""
    defaults = dict(_BLANK_FORM_DEFAULTS)
    for key in defaults:
        if key in prior and prior[key] not in (None, "", []):
            value = prior[key]
            if key in _CHOICE_KEYS and not isinstance(value, list):
                value = [value]
            defaults[key] = value
    # Normalize passengers to an int (prior context may carry it as float).
    p = defaults.get("passengers")
    if isinstance(p, (int, float)):
        defaults["passengers"] = int(p)
    return defaults
 
 
def _build_sentinel_booking_form(defaults: dict[str, Any]) -> BookingFormSpec:
    """Hardcoded fallback form when LLM emit retry exhausts. Accepts the same
    `defaults` dict that the LLM-prompt path uses, so the sentinel respects
    Modify-search prefill too."""
    return BookingFormSpec(
        surface_id="booking",
        data_model=defaults,
        components=[
            _comp("root", "Column", {"children": ["card"]}),
            _comp("card", "Card", {"child": "card_col"}),
            _comp("card_col", "Column", {"children": [
                "title", "origin", "dest", "date", "passengers", "fare", "submit",
            ]}),
            _comp("title", "Text", {"text": "Book a flight (fallback)", "variant": "h2"}),
            _comp("origin", "ChoicePicker", {"label": "Origin", "options": _AIRPORT_OPTIONS,
                                              "value": {"path": "/origin"}, "variant": "mutuallyExclusive"}),
            _comp("dest", "ChoicePicker", {"label": "Destination", "options": _AIRPORT_OPTIONS,
                                            "value": {"path": "/dest"}, "variant": "mutuallyExclusive"}),
            _comp("date", "DateTimeInput", {"label": "Departure date (YYYY-MM-DD)",
                                             "value": {"path": "/date"}, "enableDate": True}),
            _comp("passengers", "TextField", {"label": "Passengers",
                                               "value": {"path": "/passengers"}, "variant": "number"}),
            _comp("fare", "ChoicePicker", {"label": "Fare class", "options": _FARE_OPTIONS,
                                            "value": {"path": "/fare_class"}, "variant": "mutuallyExclusive"}),
            _comp("submit", "Button", {"child": "submit_label", "variant": "primary",
                                        "action": {"event": {"name": "bookingSubmit", "context": {
                                            "formId": "booking",
                                            "origin": {"path": "/origin"},
                                            "dest": {"path": "/dest"},
                                            "date": {"path": "/date"},
                                            "passengers": {"path": "/passengers"},
                                            "fare_class": {"path": "/fare_class"},
                                        }}}}),
            _comp("submit_label", "Text", {"text": "Search flights"}),
        ],
    )
 
 
# region build-form-node
async def build_form(state: MessagesState) -> dict:
    """First-turn AND Modify-search node: LLM authors the booking form.
 
    On a Modify-search turn (last action.name == 'modifySearch'), walks
    message history to recover the user's prior bookingSubmit context and
    pre-fills the form's data_model with those values. On a true first turn
    (no prior submit in history), uses blank defaults.
    """
    prior = _extract_prior_submit_context(state["messages"])
    defaults = _form_defaults_from_prior(prior)
    # If there's no prior bookingSubmit (true first turn), try to seed
    # origin/dest from the most recent human prompt — e.g. the welcome chip
    # "I want to fly LAX to JFK" should land on a form where Origin=LAX and
    # Destination=JFK are already selected. Without this seed, the LLM is
    # told to use the blank `data_model` verbatim and the user lands on a
    # form whose default values find no flights (Origin=Destination=LAX is
    # a common failure we've seen). Prior-context path skips this seed
    # because it already carries the user's last-known values.
    if not prior:
        seed = _seed_airports_from_messages(state["messages"])
        defaults.update(seed)
    system_prompt = _BUILD_FORM_SYSTEM_TMPL.replace(
        "__DATA_MODEL_DEFAULTS__", json.dumps(defaults)
    )
    base_messages = [SystemMessage(content=system_prompt)] + state["messages"]
    try:
        spec = await _emit_with_retry(BookingFormSpec, base_messages)
    except RuntimeError as err:
        _logger.error("Falling back to sentinel booking form: %s", err)
        spec = _build_sentinel_booking_form(defaults)
    return {"messages": [AIMessage(content=_wrap_envelopes(spec))]}
# endregion
 
 
# ── search_flights node ─────────────────────────────────────────────────────
 
_SEARCH_FLIGHTS_SYSTEM = """You just received a booking submission. The find_routes() tool returned the following flights:
 
{flights_json}
 
Form data (for context): {form_json}
 
Emit an A2UI v0.9 results surface using the FlightResultsSpec schema.
 
A2UI format (CRITICAL): every component is FLAT — `{{"id": "...", "component": "<ComponentName>", <props...>}}`. The component name is a plain string; props sit at the top level of the same object. Dynamic values are bare literals or `{{"path": "/ptr"}}`.
 
Allowed component names: Column, Row, Card, Text, TextField, Button, Divider, List.
 
Per-component shapes you'll need:
  Column / List: {{"children": ["id1", "id2"]}}
  Card:          {{"child": "<single-id>"}}
  Text:          {{"text": "literal", "variant": "h2"}}  (or h1/h3/body/caption)
  Button:        {{"child": "<text-id>", "variant": "primary", "action": {{"event": {{"name": "<event>", "context": {{"flightId": "<num>"}}}}}}}}
  Divider:       {{}}
 
Surface constraints:
  surface_id MUST be "results"
  data_model can be {{}}
  Exactly ONE component MUST have id "root": a Column whose children lists every flight Card id (or just ["no_flights"] when empty)
 
Build pattern (one per flight):
  card_<n>      (Card, child=col_<n>)
  col_<n>       (Column, children = [title_<n>, route_<n>, time_<n>, btn_<n>])
  title_<n>     (Text, text="<airline> flight <flight_number>", variant="h3")
  route_<n>     (Text, text="<from> → <to>  •  <duration_min> min  •  <aircraft>", variant="body")
  time_<n>      (Text, text="Depart <depart_local>  •  Arrive <arrive_local>  •  Gate <gate>", variant="caption")
  btn_<n>       (Button, child=btn_label_<n>, variant="primary",
                 action={{"event":{{"name":"flightSelect","context":{{"flightId":"<flight_number>"}}}}}})
  btn_label_<n> (Text, text="Select")
 
Empty case: components = [
  {{"id":"root", "component":"Column", "children":["no_flights"]}},
  {{"id":"no_flights", "component":"Card", "child":"empty_col"}},
  {{"id":"empty_col", "component":"Column", "children":["empty_msg","modify_btn"]}},
  {{"id":"empty_msg", "component":"Text", "text":"No flights found", "variant":"h3"}},
  {{"id":"modify_btn", "component":"Button", "child":"modify_label", "action":{{"event":{{"name":"modifySearch","context":{{"formId":"booking"}}}}}}}},
  {{"id":"modify_label", "component":"Text", "text":"Modify search"}}
]
 
Use unique ids for every component."""
 
 
_SENTINEL_RESULTS = FlightResultsSpec(
    surface_id="results",
    data_model={},
    components=[
        _comp("root", "Column", {"children": ["msg"]}),
        _comp("msg", "Card", {"child": "msg_col"}),
        _comp("msg_col", "Column", {"children": ["msg_text", "modify"]}),
        _comp("msg_text", "Text", {"text": "Results unavailable", "variant": "h3"}),
        _comp("modify", "Button", {"child": "modify_label",
                                    "action": {"event": {"name": "modifySearch",
                                                         "context": {"formId": "booking"}}}}),
        _comp("modify_label", "Text", {"text": "Modify search"}),
    ],
)
 
 
# ── confirm_booking node ────────────────────────────────────────────────────
 
_CONFIRM_BOOKING_SYSTEM = """You just received a Select event from the flight results surface. The user picked a flight; here are its details from lookup_flight():
 
{flight_json}
 
The user's prior search context (party_text, derived from passengers + fare_class on the original booking submission): {party_text}
 
The selected flight number (from the Select button's action context, used in the fallback title if flight_json is null): {flight_id}
 
Emit an A2UI v0.9 confirmation surface using the ConfirmationSpec schema.
 
A2UI format (CRITICAL): every component is FLAT — `{{"id": "...", "component": "<ComponentName>", <props...>}}`. The component name is a plain string; props sit at the top level of the same object. Dynamic values are bare literals or `{{"path": "/ptr"}}`. Exactly ONE component MUST have id "root".
 
Allowed component names: Column, Card, Text, Button, Divider.
 
Per-component shapes you'll need:
  Column:        {{"children": ["id1","id2"]}}
  Card:          {{"child": "<single-id>"}}
  Text:          {{"text": "literal", "variant": "h2"}}  (h1/h2/h3/body/caption)
  Button:        {{"child": "<text-id>", "variant": "primary", "action": {{"event": {{"name": "<event>", "context": {{"formId": "booking"}}}}}}}}
  Divider:       {{}}
 
Surface constraints:
  surface_id MUST be "confirmation"
  data_model = {{}}
 
Build this component tree exactly (use these ids):
  root         (Column, children = [card])
  card         (Card, child = card_col)
  card_col     (Column, children = [title, route_text, time_text, gate_text, divider, party_text, modify])
  title        (Text, "<airline> flight <flight_number>", variant="h2")
  route_text   (Text, "<from> → <to>  •  <duration_min> min  •  <aircraft>", variant="body")
  time_text    (Text, "Depart <depart_local>  •  Arrive <arrive_local>", variant="caption")
  gate_text    (Text, "Gate <gate>", variant="caption")
  divider      (Divider, {{}})
  party_text   (Text, "{party_text}", variant="body")  ← use the supplied string verbatim
  modify       (Button, child=modify_label, variant="primary",
                action={{"event":{{"name":"modifySearch","context":{{"formId":"booking"}}}}}})
  modify_label (Text, "Modify search")
 
If flight_json is null, omit route_text, time_text, gate_text from card_col's children and set the title to "Flight {flight_id} selected" (or "Booking selected" if {flight_id} is empty). Always include party_text + modify."""
 
 
def _build_sentinel_confirmation(flight_id: str, party_text: str) -> ConfirmationSpec:
    """Hardcoded fallback used when retry exhausts."""
    title = f"Flight {flight_id} selected" if flight_id else "Booking selected"
    return ConfirmationSpec(
        surface_id="confirmation",
        data_model={},
        components=[
            _comp("root", "Column", {"children": ["card"]}),
            _comp("card", "Card", {"child": "card_col"}),
            _comp("card_col", "Column", {"children": ["title", "party", "modify"]}),
            _comp("title", "Text", {"text": title, "variant": "h2"}),
            _comp("party", "Text", {"text": party_text, "variant": "body"}),
            _comp("modify", "Button", {"child": "modify_label",
                                       "action": {"event": {"name": "modifySearch",
                                                            "context": {"formId": "booking"}}}}),
            _comp("modify_label", "Text", {"text": "Modify search"}),
        ],
    )
 
 
async def confirm_booking(state: MessagesState) -> dict:
    """Post-Select node: look up the chosen flight, recover party context from
    prior submit, emit confirmation surface."""
    last = state["messages"][-1]
    select_data = _parse_submit_payload(getattr(last, "content", "")) or {}
    flight_id_raw = select_data.get("flightId") or select_data.get("flight_id") or ""
    flight_id = flight_id_raw.upper() if isinstance(flight_id_raw, str) else ""
 
    flight: dict[str, Any] | None = None
    if flight_id:
        try:
            flight = await lookup_flight.ainvoke({"flight_number": flight_id})
        except Exception as err:  # noqa: BLE001 — demo robustness
            _logger.warning("lookup_flight failed for %s: %s", flight_id, err)
 
    prior = _extract_prior_submit_context(state["messages"])
    party_text = _format_party(prior)
 
    base_messages = [
        SystemMessage(content=_CONFIRM_BOOKING_SYSTEM.format(
            flight_json=json.dumps(flight, indent=2) if flight else "null",
            party_text=party_text,
            flight_id=flight_id,
        )),
        HumanMessage(content=f"Emit the confirmation surface for flight {flight_id or '(unknown)'}."),
    ]
    try:
        spec = await _emit_with_retry(ConfirmationSpec, base_messages)
    except RuntimeError as err:
        _logger.error("Falling back to sentinel confirmation surface: %s", err)
        spec = _build_sentinel_confirmation(flight_id, party_text)
    return {"messages": [AIMessage(content=_wrap_envelopes(spec))]}
 
 
def _unwrap_literal(v: Any) -> Any:
    """Unwrap a legacy literal wrapper ({literalString|literalNumber|literalBoolean: <v>}).
    v0.9 context values arrive as bare literals, which pass through unchanged."""
    if isinstance(v, dict):
        for k in ("literalString", "literalNumber", "literalBoolean"):
            if k in v:
                return v[k]
    return v
 
 
def _scalarize(v: Any) -> Any:
    """Collapse a ChoicePicker string-array value ("['LAX']") to its first
    element. v0.9 pickers bind to string arrays in the data model, so
    path-bound action context values may arrive as single-element lists."""
    if isinstance(v, list):
        return v[0] if v else ""
    return v
 
 
def _parse_submit_payload(content: str) -> dict[str, Any] | None:
    """Extract the form-data dict from an A2UI action-message content.
 
    Chat-lib sends:
      {"version":"v0.9","action":{"name":"...","surfaceId":"...",
        "context":{"formId":"booking","origin":["LAX"], ...}}}
 
    Context values are plain literals (path bindings are resolved
    client-side); picker values arrive as string arrays and are
    scalarized. Legacy literal wrappers are still unwrapped tolerantly.
    """
    try:
        payload = json.loads(content)
    except (json.JSONDecodeError, TypeError):
        return None
    if not isinstance(payload, dict):
        return None
    action = payload.get("action")
    if not isinstance(action, dict):
        return None
    ctx = action.get("context", {})
    if not isinstance(ctx, dict):
        return None
    return {k: _scalarize(_unwrap_literal(v)) for k, v in ctx.items()}
 
 
async def search_flights(state: MessagesState) -> dict:
    """Post-submit node: call find_routes, emit results A2UI surface."""
    last = state["messages"][-1]
    form_data = _parse_submit_payload(getattr(last, "content", "")) or {}
    origin = (form_data.get("origin") or "").upper()
    dest = (form_data.get("dest") or "").upper()
    flights: list[dict[str, Any]] = []
    if origin and dest and origin != dest:
        try:
            flights = await find_routes.ainvoke({"from_code": origin, "to_code": dest})
        except Exception as err:  # noqa: BLE001 — demo robustness
            _logger.warning("find_routes failed for %s→%s: %s", origin, dest, err)
 
    base_messages = [
        SystemMessage(content=_SEARCH_FLIGHTS_SYSTEM.format(
            flights_json=json.dumps(flights, indent=2),
            form_json=json.dumps(form_data, indent=2),
        )),
        HumanMessage(content=f"Emit the results surface for {origin}→{dest}."),
    ]
    try:
        spec = await _emit_with_retry(FlightResultsSpec, base_messages)
    except RuntimeError as err:
        _logger.error("Falling back to sentinel results surface: %s", err)
        spec = _SENTINEL_RESULTS
    return {"messages": [AIMessage(content=_wrap_envelopes(spec))]}
 
 
# ── Routing + compile ───────────────────────────────────────────────────────
 
def _is_submit_event(content: str) -> bool:
    """True iff the content is an A2UI action message named bookingSubmit."""
    try:
        payload = json.loads(content)
    except (json.JSONDecodeError, TypeError):
        return False
    if not isinstance(payload, dict):
        return False
    action = payload.get("action")
    return (
        isinstance(action, dict)
        and action.get("name") == "bookingSubmit"
    )
 
 
def _is_flight_select_event(content: str) -> bool:
    """True iff the content is an A2UI action message named flightSelect."""
    try:
        payload = json.loads(content)
    except (json.JSONDecodeError, TypeError):
        return False
    if not isinstance(payload, dict):
        return False
    action = payload.get("action")
    return (
        isinstance(action, dict)
        and action.get("name") == "flightSelect"
    )
 
 
# Match "<ORIGIN> to <DEST>" / "<ORIGIN> -> <DEST>" / "<ORIGIN> → <DEST>"
# where both are 3-letter IATA codes from AIRPORT_CODES. Anchored to word
# boundaries so we don't false-match "BOSTON" or arbitrary capitals.
_AIRPORT_CODES_RE = "|".join(re.escape(code) for code in AIRPORT_CODES)
_AIRPORT_PAIR_PATTERN = re.compile(
    rf"\b({_AIRPORT_CODES_RE})\b\s*(?:to|->|→|-)\s*\b({_AIRPORT_CODES_RE})\b",
    re.IGNORECASE,
)
 
 
def _seed_airports_from_messages(messages: list[Any]) -> dict[str, str]:
    """Extract an (origin, dest) airport pair from the most recent human
    message. Used by build_form on a fresh first turn to pre-fill the form
    when the user's prompt explicitly mentions a route (e.g. the welcome
    chip "I want to fly LAX to JFK").
 
    Returns {"origin": [<CODE>], "dest": [<CODE>]} on a hit (string arrays,
    matching the ChoicePicker data-model shape); {} when no recognized pair
    appears. Both codes must be in AIRPORT_CODES; we never seed an airport
    the form's dropdown can't render."""
    for msg in reversed(messages):
        # Only inspect human messages — AI surfaces and action JSON shouldn't
        # be parsed for seed values.
        if getattr(msg, "type", None) != "human":
            continue
        content = getattr(msg, "content", None)
        if not isinstance(content, str):
            continue
        # Action messages also flow through as human-role; their content
        # is JSON. Cheap filter: real prompts don't start with '{'.
        if content.lstrip().startswith("{"):
            continue
        match = _AIRPORT_PAIR_PATTERN.search(content)
        if match:
            origin = match.group(1).upper()
            dest = match.group(2).upper()
            if origin == dest:
                # Same-airport "route" can't possibly find flights; skip.
                continue
            return {"origin": [origin], "dest": [dest]}
    return {}
 
 
def _extract_prior_submit_context(messages: list[Any]) -> dict[str, Any]:
    """Walk back, find the most recent bookingSubmit A2UI action message;
    return its unwrapped context dict (origin/dest/date/passengers/fare_class).
    Returns {} if not found."""
    for msg in reversed(messages):
        content = getattr(msg, "content", None)
        if not isinstance(content, str):
            continue
        try:
            payload = json.loads(content)
        except (json.JSONDecodeError, TypeError):
            continue
        if (
            isinstance(payload, dict)
            and isinstance(payload.get("action"), dict)
            and payload["action"].get("name") == "bookingSubmit"
        ):
            ctx = payload["action"].get("context", {})
            if not isinstance(ctx, dict):
                return {}
            return {k: _scalarize(_unwrap_literal(v)) for k, v in ctx.items()}
    return {}
 
 
def _format_party(prior: dict[str, Any]) -> str:
    """Pretty-print passenger count + fare class for the confirmation text.
    Tolerant of missing fields."""
    parts: list[str] = []
    n = prior.get("passengers")
    if isinstance(n, (int, float)) and n > 0:
        parts.append(f"{int(n)} passenger" + ("" if int(n) == 1 else "s"))
    fare = prior.get("fare_class")
    if isinstance(fare, str) and fare:
        parts.append(fare)
    return "  •  ".join(parts) if parts else "(party details unavailable)"
 
 
# region route
def route(state: MessagesState) -> Command[Literal["build_form", "search_flights", "confirm_booking"]]:
    """Inspect the last message — submit event → search_flights, flight-select
    event → confirm_booking, else build_form."""
    last_content = getattr(state["messages"][-1], "content", "") if state["messages"] else ""
    if _is_submit_event(last_content):
        return Command(goto="search_flights")
    if _is_flight_select_event(last_content):
        return Command(goto="confirm_booking")
    return Command(goto="build_form")
# endregion
 
 
# ── generate_title node (inline; matches Pattern D from spec
#     2026-05-19-llm-generated-labels-design.md) ──────────────────────────────
 
_TITLE_PROMPT = (
    "In 3-5 words, summarize what the user is asking about. "
    "Output ONLY the title — no quotes, no period, no prefix."
)
_TITLE_MODEL = "gpt-5-mini"
 
 
async def generate_title(state: MessagesState, config) -> dict:
    """Background title generation: on the first turn, summarize the user's
    intent into 3-5 words and persist to LangGraph thread metadata so the
    sidenav shows something meaningful instead of a UUID slice.
 
    Idempotent — skips when metadata.title already exists. Errors are
    swallowed (title is a UX nicety, never a blocker). Runs after the
    user-visible terminal node so it never blocks the response. See spec
    2026-05-19-llm-generated-labels-design.md (originally `thread_title`,
    converged to `title` for parity with the canonical demo + adapter).
    """
    thread_id = (config.get("configurable") or {}).get("thread_id")
    if not thread_id:
        return {}
    # url=None lets the SDK use its in-process ASGI transport when the
    # call originates from inside a LangGraph server graph (always the
    # case here). The old fallback to localhost:2024 forced an HTTP
    # round-trip that fails on the prod runtime container. See PR #493.
    sdk_url = os.environ.get("LANGGRAPH_API_URL")
    try:
        client = get_client(url=sdk_url)
        thread = await client.threads.get(thread_id)
        if (thread.get("metadata") or {}).get("title"):
            return {}
        first_user = next(
            (m for m in state["messages"] if getattr(m, "type", None) == "human"),
            None,
        )
        if not first_user or not isinstance(first_user.content, str):
            return {}
        # Skip action-message JSON (those flow as human-role too)
        if first_user.content.lstrip().startswith("{"):
            return {}
        llm = ChatOpenAI(model=_TITLE_MODEL, temperature=0)
        response = await llm.ainvoke([
            SystemMessage(content=_TITLE_PROMPT),
            HumanMessage(content=first_user.content),
        ])
        title = (response.content or "").strip().strip('"').strip("'")[:80]
        if title:
            await client.threads.update(thread_id, metadata={"title": title})
    except Exception as err:  # noqa: BLE001 — title is a UX nicety; never block
        _logger.warning("Thread title generation failed: %s", err)
    return {}
 
 
# region graph-wiring
_builder = StateGraph(MessagesState)
_builder.add_node("route", route)
_builder.add_node("build_form", build_form)
_builder.add_node("search_flights", search_flights)
_builder.add_node("confirm_booking", confirm_booking)
_builder.add_node("generate_title", generate_title)
_builder.set_entry_point("route")
_builder.add_edge("build_form", "generate_title")
_builder.add_edge("search_flights", "generate_title")
_builder.add_edge("confirm_booking", "generate_title")
_builder.add_edge("generate_title", END)
 
graph = _builder.compile(checkpointer=MemorySaver())
# endregion

Build from source

npx nx build cockpit-ag-ui-a2ui-react
npx nx e2e cockpit-ag-ui-a2ui-react

The uncached e2e target prepares frozen Python 3.12 dependencies, verifies an isolated installed native application once, and runs the browser flow against actual compiled graph events with deterministic model responses. Each browser conversation owns its canonical child and checkpointer. Local fixture controls and proof artifacts are excluded from Code and the application bundle. This verifies local native execution; live model-provider behavior remains separate.

Looking for something specific?