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
Section titled “Interceptors”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.
How interceptors work
Section titled “How interceptors work”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:
| Prop | Called when the agent calls |
|---|---|
onBeforeCreateSurface | create_surface |
onBeforeUpdateComponents | update_components |
onBeforeUpdateDataModel | update_data_model |
onBeforeDeleteSurface | delete_surface |
onBeforeCreateSurface
Section titled “onBeforeCreateSurface”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: '' }; }}>onBeforeUpdateComponents
Section titled “onBeforeUpdateComponents”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: '' }; }}>onBeforeUpdateDataModel
Section titled “onBeforeUpdateDataModel”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: '' }; }}>onBeforeDeleteSurface
Section titled “onBeforeDeleteSurface”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: '' };}}Combining multiple interceptors
Section titled “Combining multiple interceptors”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> );}Additional Capabilities
Section titled “Additional Capabilities”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.
Common patterns
Section titled “Common patterns”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
additionalCapabilitieswith 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.
Event Handling
Section titled “Event Handling”FreesailProvider exposes two event callbacks for monitoring the connection lifecycle. These are useful for showing connection status UI, logging, and triggering reconnect flows.
onConnectionChange
Section titled “onConnectionChange”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); }}>onError
Section titled “onError”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); }}>Building a connection status indicator
Section titled “Building a connection status indicator”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> );}Handling disconnections gracefully
Section titled “Handling disconnections gracefully”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> );}Putting It All Together
Section titled “Putting It All Together”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> );}Summary
Section titled “Summary”| Feature | Purpose | Prop(s) |
|---|---|---|
| Interceptors | Block or validate agent operations before they apply | onBeforeCreateSurface, onBeforeUpdateComponents, onBeforeUpdateDataModel, onBeforeDeleteSurface |
| Additional capabilities | Advertise environment context and feature flags to the agent via get_catalogs | additionalCapabilities |
| Event handlers | React to connection state changes and transport errors | onConnectionChange, onError |