Skip to content

Advanced Techniques

This guide covers advanced FreesailProvider features that give the frontend layer control over what agents can do, how they communicate their capabilities, and how the application reacts to connection lifecycle events.


Interceptors let the React app intercept and optionally block incoming agent operations before they are applied. This is useful for enforcing client-side rules the agent cannot know about — permission boundaries, UI state guards, rate limits, or schema constraints specific to the deployed environment.

Each onBefore* prop on FreesailProvider is called synchronously before the corresponding operation is applied to the surface manager. Return an object with allowed: false and a message to block the operation — the message is forwarded to the agent as an error payload so the LLM can react and recover. Return { allowed: true, message: '' } (or omit the message) to permit the operation.

type SurfaceInterceptorResult = {
allowed: boolean;
message: string;
};

The four interceptors mirror the four agent-driven surface operations:

PropCalled when the agent calls
onBeforeCreateSurfacecreate_surface
onBeforeUpdateComponentsupdate_components
onBeforeUpdateDataModelupdate_data_model
onBeforeDeleteSurfacedelete_surface

Called with (surfaceId: string, catalogId: string) before a new agent-managed surface is created.

<FreesailProvider
catalogs={[StandardCatalog]}
onBeforeCreateSurface={(surfaceId, catalogId, _sendDataModel, surfaceManager) => {
// Only allow surfaces with known IDs
const allowedSurfaces = ['workspace', 'dashboard', 'editor'];
if (!allowedSurfaces.includes(surfaceId)) {
return {
allowed: false,
message: `Surface "${surfaceId}" is not permitted in this application.`,
};
}
return { allowed: true, message: '' };
}}
>

Called with (surfaceId: string, components: A2UIComponent[]) before the agent replaces the component tree on a surface.

<FreesailProvider
catalogs={[StandardCatalog]}
onBeforeUpdateComponents={(surfaceId, components) => {
// Guard against oversized updates that could degrade performance
if (components.length > 30) {
return {
allowed: false,
message: `Component tree too large (${components.length} nodes). Keep updates under 30 components.`,
};
}
// Prevent the agent touching a surface that's locked by the user
if (lockedSurfaces.has(surfaceId)) {
return {
allowed: false,
message: `Surface "${surfaceId}" is currently locked by the user.`,
};
}
return { allowed: true, message: '' };
}}
>

Called with (surfaceId: string, path: string, value: unknown) before the agent writes a value to the surface data model.

<FreesailProvider
catalogs={[StandardCatalog]}
onBeforeUpdateDataModel={(surfaceId, path, value) => {
// Prevent the agent from overwriting a path the user is actively editing
if (path === '/form/draft' && userIsEditing) {
return {
allowed: false,
message: 'User is currently editing the form. Data model update deferred.',
};
}
return { allowed: true, message: '' };
}}
>

Called with (surfaceId: string) before the agent removes a surface.

<FreesailProvider
catalogs={[StandardCatalog]}
onBeforeDeleteSurface={(surfaceId) => {
// Confirm that any unsaved work has been flushed before the agent tears down the surface
if (unsavedChanges.has(surfaceId)) {
flushChanges(surfaceId);
// Allow the deletion to proceed after saving
}
return { allowed: true, message: '' };
}}
>

Sending a validation message without blocking

Section titled “Sending a validation message without blocking”

Returning { allowed: true, message: '...' } (with a non-empty message) allows the operation but also delivers the message to the agent. Use this for soft warnings — the agent receives the message and can adjust future calls without the current one being rejected.

onBeforeUpdateComponents={(surfaceId, components) => {
if (components.length > 50) {
return {
allowed: true,
message: `Heads up: this update contains ${components.length} components. Prefer smaller, incremental updates for better performance.`,
};
}
return { allowed: true, message: '' };
}}

Interceptors compose naturally — define each independently and they all fire in order:

function App() {
const [lockedSurfaces] = useState<Set<string>>(new Set());
const [unsavedChanges] = useState<Set<string>>(new Set());
return (
<ReactUI.FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog, ChatCatalog]}
onBeforeCreateSurface={(surfaceId, _catalogId, _sendDataModel, surfaceManager) => {
if (!isAuthorised(surfaceId)) {
return { allowed: false, message: `Not authorised to create "${surfaceId}".` };
}
return { allowed: true, message: '' };
}}
onBeforeUpdateComponents={(surfaceId, components) => {
if (lockedSurfaces.has(surfaceId)) {
return { allowed: false, message: `"${surfaceId}" is locked.` };
}
return { allowed: true, message: '' };
}}
onBeforeUpdateDataModel={(surfaceId, path) => {
if (path.startsWith('/readonly/')) {
return { allowed: false, message: `Path "${path}" is read-only.` };
}
return { allowed: true, message: '' };
}}
onBeforeDeleteSurface={(surfaceId) => {
if (unsavedChanges.has(surfaceId)) {
flushChanges(surfaceId);
}
return { allowed: true, message: '' };
}}
>
<MainLayout />
</ReactUI.FreesailProvider>
);
}

The additionalCapabilities prop lets the frontend advertise extra context to the agent alongside the standard catalog list. When the agent calls get_catalogs, the response includes these key/value pairs merged into the catalog metadata.

This is the primary mechanism for passing session-level context that the agent needs to tailor its behaviour — things like feature flags, user role, application mode, or environment — without encoding that context into a catalog schema.

<FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog]}
additionalCapabilities={{
userRole: 'admin',
featureFlags: { darkMode: true, betaComponents: false },
appMode: 'production',
maxSurfaces: 3,
}}
>

The agent runtime’s system prompt includes the capabilities object as part of the catalog metadata, so the LLM can read it at the start of each session. Agents should treat capabilities as advisory — they describe the environment the agent is operating in, not commands.

Feature flags — tell the agent which optional components or functions are available:

additionalCapabilities={{
features: {
chartsEnabled: true,
exportEnabled: false,
advancedFilters: userPlan === 'pro',
},
}}

User context — provide role and permission information the agent can use to constrain its output:

additionalCapabilities={{
user: {
role: currentUser.role, // 'viewer' | 'editor' | 'admin'
locale: navigator.language, // 'en-GB'
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
}}

Application state hints — let the agent know which surfaces it is allowed to create in the current context:

additionalCapabilities={{
allowedSurfaces: ['workspace', 'editor'],
maxComponents: 80,
}}

Tip: Pair additionalCapabilities with interceptors for defence-in-depth: use capabilities to guide the agent’s intent, and interceptors to enforce hard boundaries on what it can actually do.


FreesailProvider exposes two event callbacks for monitoring the connection lifecycle. These are useful for showing connection status UI, logging, and triggering reconnect flows.

Called whenever the SSE connection is established or lost. Receives a boolean: true when connected, false when disconnected.

<FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog]}
onConnectionChange={(connected) => {
console.log(connected ? 'Gateway connected' : 'Gateway disconnected');
setIsOnline(connected);
}}
>

Called when a transport error or message parse error occurs. Receives an Error object. This fires in addition to onConnectionChange — use it for logging or surfacing errors to the user.

<FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog]}
onError={(error) => {
console.error('Freesail transport error:', error.message);
Sentry.captureException(error);
}}
>

Use onConnectionChange together with the useConnectionStatus() hook to build a live status indicator:

function ConnectionBadge() {
const { isConnected } = ReactUI.useConnectionStatus();
return (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
fontSize: '12px',
color: isConnected ? 'var(--freesail-success, #22c55e)' : 'var(--freesail-error, #ef4444)',
}}>
<span style={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'currentColor',
}} />
{isConnected ? 'Connected' : 'Reconnecting…'}
</div>
);
}

The gateway’s reconnect grace period (default 180 seconds) means brief disconnections — page refreshes, network blips — don’t require the agent to restart. The provider restores session state from sessionStorage automatically. Use onConnectionChange to show a banner during the gap:

function App() {
const [showBanner, setShowBanner] = useState(false);
return (
<ReactUI.FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog, ChatCatalog]}
onConnectionChange={(connected) => {
setShowBanner(!connected);
}}
>
{showBanner && (
<div className="reconnecting-banner">
Reconnecting to gateway…
</div>
)}
<MainLayout />
</ReactUI.FreesailProvider>
);
}

A production FreesailProvider setup combining all three features — interceptors, capabilities, and event handlers:

import { ReactUI } from 'freesail';
import { StandardCatalog } from '@freesail/standard-catalog';
import { ChatCatalog } from '@freesail/chat-catalog';
function App() {
const { user, featureFlags } = useAppContext();
const [isConnected, setIsConnected] = useState(false);
return (
<ReactUI.FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[StandardCatalog, ChatCatalog]}
// Advertise environment context to the agent
additionalCapabilities={{
user: { role: user.role, locale: user.locale },
features: featureFlags,
allowedSurfaces: user.role === 'admin'
? ['workspace', 'dashboard', 'editor', 'reports']
: ['workspace', 'dashboard'],
}}
// Enforce permission boundaries
onBeforeCreateSurface={(surfaceId, _catalogId, _sendDataModel, surfaceManager) => {
const allowed = getPermittedSurfaces(user.role);
if (!allowed.includes(surfaceId)) {
return {
allowed: false,
message: `User role "${user.role}" cannot create surface "${surfaceId}".`,
};
}
return { allowed: true, message: '' };
}}
onBeforeUpdateComponents={(surfaceId, components) => {
if (components.length > 80) {
return {
allowed: false,
message: `Too many components (${components.length}). Maximum is 80.`,
};
}
return { allowed: true, message: '' };
}}
onBeforeUpdateDataModel={(surfaceId, path) => {
if (path.startsWith('/system/')) {
return { allowed: false, message: `Path "${path}" is reserved.` };
}
return { allowed: true, message: '' };
}}
// Monitor the connection
onConnectionChange={setIsConnected}
onError={(error) => logger.error('Freesail error', error)}
>
{!isConnected && <ReconnectingBanner />}
<MainLayout />
</ReactUI.FreesailProvider>
);
}

FeaturePurposeProp(s)
InterceptorsBlock or validate agent operations before they applyonBeforeCreateSurface, onBeforeUpdateComponents, onBeforeUpdateDataModel, onBeforeDeleteSurface
Additional capabilitiesAdvertise environment context and feature flags to the agent via get_catalogsadditionalCapabilities
Event handlersReact to connection state changes and transport errorsonConnectionChange, onError