@servicetitan/assist-utils
v1.2.0
Published
Assist utilities for host-MFE communication
Readme
Atlas Assist Utils Documentation
Overview
The @servicetitan/assist-utils package provides a comprehensive framework for cross-MFE (Micro-Frontend) communication, enabling different applications to share tools and data seamlessly. Built on top of the Atlas Registry system, it uses React hooks and React IoC for dependency injection.
Table of Contents
- Setup and Installation
- Architecture Overview
- Core Concepts
- Atlas Registry System
- Tool Registry
- Data Registry
- Usage Examples
- Advanced Patterns
- Best Practices
Setup and Installation
Provider Hierarchy
The Atlas Registry system requires a specific setup to ensure all applications (Host, MFEs, and TI-Assist) share the same EventBus and registry.
1. Host Application Setup
The Host application is responsible for creating the shared EventBus and registering the Atlas Registry.
First, create the EventBus and provide it via React IoC:
import { Provider } from '@servicetitan/react-ioc';
import { EventBus, EVENT_BUS_TOKEN } from '@servicetitan/shared-data-registry';
import { AtlasRegistryProvider, AtlasProvider } from '@servicetitan/assist-utils';
function App() {
return (
<Provider singletons={[{ provide: EVENT_BUS_TOKEN, useValue: new EventBus() }]}>
{/* EventBus created by Host */}
<AtlasRegistryProvider>
{/* This registers the AtlasRegistry with the EventBus */}
<AtlasProvider>
{/* Your host application code */}
<HostComponents />
{/* MFEs will be loaded here */}
</AtlasProvider>
</AtlasRegistryProvider>
</Provider>
);
}Important:
ProviderwithEVENT_BUS_TOKEN- Creates the shared EventBus. Only the Host application creates this.AtlasRegistryProvider- Registers the Atlas Registry with the EventBus. Only use this in the Host application.AtlasProvider- Provides access toAtlasRegistryStore,AtlasRegistryReader, andAtlasRegistryWriter. Required for using hooks likeuseAtlasContextRegisteranduseAtlasHandlerRegistry.
2. MFE (Micro-Frontend) Setup
MFEs do NOT need AtlasRegistryProvider - they inherit the EventBus from the Host.
Wrap only the components that need registry access with AtlasProvider:
import { AtlasProvider } from '@servicetitan/assist-utils';
// Wrap specific components that use Atlas hooks
export const MfeCommunication = () => {
return (
<AtlasProvider>
<MfeCommunicationComponent />
</AtlasProvider>
);
};
// Your MFE root doesn't need AtlasProvider if not all components use it
function App() {
return (
<div>
<MfeCommunication /> {/* Only this uses AtlasProvider */}
<OtherComponents /> {/* These don't need it */}
</div>
);
}Note: You can wrap at the root level if all/most components need registry access, or wrap individual components for more granular control.
3. TI-Assist Setup
Same as MFEs - wrap components that need registry access with AtlasProvider:
import { AtlasProvider } from '@servicetitan/assist-utils';
export const AssistPanel = () => {
return (
<AtlasProvider>
{/* Components using useAtlasToolInvoke, useAtlasDataConsumer, etc. */}
<AssistComponents />
</AtlasProvider>
);
};How It Works
Host Application
└── Provider (creates EventBus via EVENT_BUS_TOKEN)
└── AtlasRegistryProvider (registers 'atlas-host-communication-registry')
└── AtlasProvider (provides Store, Reader, Writer)
├── Host Components (can use hooks)
├── MFE1
│ └── AtlasProvider (connects to same EventBus)
│ └── MFE1 Components (can use hooks)
├── MFE2
│ └── AtlasProvider (connects to same EventBus)
│ └── MFE2 Components (can use hooks)
└── TI-Assist
└── AtlasProvider (connects to same EventBus)
└── Assist Components (can use hooks)Key Points:
- Host creates the EventBus via
ProviderwithEVENT_BUS_TOKEN AtlasRegistryProviderregisters the Atlas Registry with ID:'atlas-host-communication-registry'- All applications share the same EventBus through the shared registry ID
- Only the Host uses
Provider(for EventBus) andAtlasRegistryProvider(for registry) - All applications (Host, MFEs, TI-Assist) use
AtlasProviderto access the registry - This ensures cross-MFE communication works seamlessly
Architecture Overview
System Components
┌─────────────────────────────────────────────────────────────────┐
│ Atlas Registry - (assist-utils) │
│ (Centralized message bus for cross-MFE communication) │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────┴─────────────┐
│ │
┌───────▼──────┐ ┌──────▼─────┐
│ Tool Registry│ │ Data │
│ │ │ Registry │
└───────┬──────┘ └──────┬─────┘
│ │
┌───────────┼───────────────────────────┼───────────┐
│ │ │ │
┌───▼────┐ ┌───▼────┐ ┌─────▼─────┐ ┌────▼─────┐ ┌───▼────┐
│ Host │ │ MFE1 │ │ MFE2 │ │ TI-Assist│ │ MFE3 │
│ App │ │ │ │ │ │ │ │ │
└────────┘ └────────┘ └───────────┘ └──────────┘ └────────┘Communication Flow
- Registration Phase: Each MFE registers its tools and data
- Discovery Phase: Consumers discover available resources via the registry
- Invocation Phase: Consumers invoke tools and subscribe to data
- Update Phase: Changes propagate automatically through the registry
Core Concepts
1. Registry Pattern
The Atlas Registry uses a publish-subscribe pattern where:
- Producers register resources (tools, data)
- Consumers discover and use these resources
- Registry maintains the connection and ensures data flow
2. React IoC (Inversion of Control)
All registry access uses dependency injection via @servicetitan/react-ioc:
const [reader] = useDependencies(AtlasRegistryReader);
const [writer] = useDependencies(AtlasRegistryWriter);Why IoC?
- Centralized state management across MFEs
- Single source of truth for all registered resources
- Automatic cleanup and memory management
3. Serialization Strategy
Serializable Data → Stored in Atlas Registry
This hybrid approach allows:
- Metadata to be discoverable across all MFEs
- Functions to be directly accessible without serialization issues
Atlas Registry System
AtlasRegistryWriter
Purpose: Add items to the registry
import { useDependencies } from '@servicetitan/react-ioc';
import { AtlasRegistryWriter } from '@servicetitan/assist-utils';
const [writer] = useDependencies(AtlasRegistryWriter);
// Add an item
const id = writer.add({
type: 'MY_MESSAGE_TYPE',
data: { foo: 'bar' }
});
// Remove an item
writer.remove(id);AtlasRegistryReader
Purpose: Read items from the registry
import { useDependencies } from '@servicetitan/react-ioc';
import { AtlasRegistryReader } from '@servicetitan/assist-utils';
const [reader] = useDependencies(AtlasRegistryReader);
// Subscribe to registry updates
useEffect(() => {
reader.subscribe(setItems);
return () => reader.unsubscribe(setItems);
}, [reader]);useRegistry Hook
Purpose: Combined reader/writer with message sending
import { useRegistry } from '@servicetitan/assist-utils';
const { items, sendMessage } = useRegistry();
// Send a message to all MFEs
sendMessage({
type: 'USER_ACTION',
data: { action: 'click', target: 'button' },
});Tool Registry
Tools enable cross-MFE function execution - one MFE can invoke functions registered by another MFE.
Architecture
┌──────────────┐ ┌──────────────┐
│ Provider │ │ Consumer │
│ (Host) │ │ (TI-Assist) │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. Register Tool │
│ useAtlasHandlerRegistry │
│ │
├────────────────────────────────────▶
│ Metadata → Atlas Registry │
│ Handler │
│ │
│ 2. Discover Tools
│ useAtlasToolInvoke
│ │
│ 3. Invoke Tool
│◀───────────────────────────────────┤
│ │
│ 4. Execute Handler │
│ │
│ 5. Return Result │
├────────────────────────────────────▶useAtlasHandlerRegistry (Provider Side)
Purpose: Register a function that other MFEs can invoke
import { useAtlasHandlerRegistry } from '@servicetitan/assist-utils';
useAtlasHandlerRegistry('calculateTotal', {
name: 'Calculate Total',
description: 'Calculates the total price with tax',
assistantId: 'assistant-123', // Optional: Associate tool with specific assistant
parameters: [
{
name: 'price',
type: 'number',
description: 'Base price',
required: true
},
{
name: 'taxRate',
type: 'number',
description: 'Tax rate (0.0 to 1.0)',
required: true
},
],
handler: ({ price, taxRate }) => {
const tax = price * taxRate;
const total = price + tax;
return {
subtotal: price,
tax: parseFloat(tax.toFixed(2)),
total: parseFloat(total.toFixed(2)),
};
},
});How it works:
- Metadata (name, description, parameters) → Stored in Atlas Registry
- Handler function executes directly via the registry
- Automatic cleanup on component unmount
useAtlasToolInvoke (Consumer Side)
Purpose: Discover and invoke tools from other MFEs
import { useAtlasToolInvoke } from '@servicetitan/assist-utils';
const { invokeTool, availableTools } = useAtlasToolInvoke();
// List all available tools
const tools = availableTools();
console.log(tools); // [{ toolKey: 'calculateTotal', name: 'Calculate Total', ... }]
// Invoke a tool
const result = await invokeTool('calculateTotal', {
price: 100,
taxRate: 0.15,
});
console.log(result); // { subtotal: 100, tax: 15, total: 115 }How it works:
- Subscribes to Atlas Registry for tool metadata
invokeTool()communicates with provider via the registry- Provider's handler executes and returns result via Promise
- 5-second timeout for safety (configurable)
Tool Invocation Flow
// Step 1: Consumer invokes tool
invokeTool('calculateTotal', { price: 100, taxRate: 0.15 })
// Step 3: Provider handler executes
handler({ price: 100, taxRate: 0.15 }) // Returns result
// Step 5: Consumer receives result
// Promise resolves with { subtotal: 100, tax: 15, total: 115 }useAtlasSystemMessage (MFE to TI-Assist Communication)
Purpose: Send system messages from MFEs to TI-Assist
System messages allow MFEs to provide context or trigger actions in the AI assistant without showing messages in the user-facing chat interface.
import { useAtlasSystemMessage } from '@servicetitan/assist-utils';
const MyMFEComponent = () => {
const sendSystemMessage = useAtlasSystemMessage();
const handleExport = () => {
// Send context to TI-Assist about user action
sendSystemMessage({
text: 'User initiated data export',
context: {
action: 'export',
selectedItems: [1, 2, 3],
currentView: 'dashboard'
},
assistantId: 'assistant-123', // Optional
interactionId: 'interaction-456' // Required for message creation
});
};
return <button onClick={handleExport}>Export Data</button>;
};How it works:
- MFE calls
sendSystemMessage()with payload - Message is sent via Atlas Registry with type
ATLAS_SYSTEM_MESSAGE - TI-Assist's MessageCreatorStore listens for these messages
- If
interactionIdis provided, a SystemMessage is created via API - The AI assistant receives the context and can respond accordingly
Parameters:
text(required): Description of the system event or contextcontext(optional): Additional structured data about the eventassistantId(optional): Specific assistant to targetinteractionId(required): The interaction ID where the message should be created
Important Notes:
- System messages are NOT displayed in the chat UI
- They provide background context to the AI assistant
interactionIdis required for the message to be created- Messages are automatically cleaned up from the registry after 2 seconds
Data Registry
Data Registry enables real-time data synchronization - changes in one MFE automatically reflect in all subscribed MFEs.
Architecture
┌──────────────┐ ┌──────────────┐
│ Provider │ │ Consumer │
│ (Host) │ │ (TI-Assist) │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. Register Data │
│ useAtlasContextRegister │
│ { key, data } │
│ │
├────────────────────────────────────▶
│ Atlas Registry │
│ │
│ 2. Subscribe
│ useAtlasDataConsumer
│ │
│ 3. Data Updates (state change) │
│ │
├────────────────────────────────────▶
│ Auto-sync via Registry │
│ │
│ 4. React re-renders
│ with new datauseAtlasContextRegister (Provider Side)
Purpose: Share live state data with other MFEs
import { useAtlasContextRegister } from '@servicetitan/assist-utils';
const [counter, setCounter] = useState(0);
const [status, setStatus] = useState('idle');
// Share live state - automatically updates consumers
useAtlasContextRegister({
key: 'hostLiveState',
description: 'Host application live state',
assistantId: 'assistant-123', // Optional: Associate data with specific assistant
payload: { counter, status },
});Advanced: Memoization for Performance
const [counter, setCounter] = useState(0);
const [status, setStatus] = useState('idle');
const [otherData, setOtherData] = useState('...');
// Memoize to prevent unnecessary registry updates
const sharedData = useMemo(
() => ({ counter, status }),
[counter, status]
);
useAtlasContextRegister({
key: 'hostLiveState',
description: 'Host application live state',
payload: sharedData, // Only updates when counter or status changes
});How it works:
- Registers data with unique key in Atlas Registry
- Every time
payloadchanges, registry is updated - All consumers automatically receive the update
- Automatic cleanup on unmount
useAtlasDataConsumer (Consumer Side)
Purpose: Subscribe to live data from other MFEs
import { useAtlasDataConsumer } from '@servicetitan/assist-utils';
// Subscribe to data by key
const liveState = useAtlasDataConsumer<{ counter: number; status: string }>('hostLiveState');
// Use in component
return (
<div>
<p>Counter: {liveState?.counter}</p>
<p>Status: {liveState?.status}</p>
</div>
);Type Safety:
interface HostState {
counter: number;
status: string;
}
const liveState = useAtlasDataConsumer<HostState>('hostLiveState');
// liveState is typed as HostState | undefined
if (liveState) {
console.log(liveState.counter); // TypeScript knows this is a number
}How it works:
- Subscribes to Atlas Registry items
- Filters for items with matching key
- Returns the latest data
- Automatically updates when provider changes data
- Returns
undefinedif data doesn't exist yet
useAtlasDataList (Advanced)
Purpose: Get list of all available data sources
import { useAtlasDataList } from '@servicetitan/assist-utils';
const dataSources = useAtlasDataList();
console.log(dataSources);
// [
// { key: 'hostLiveState', description: 'Host application live state' },
// { key: 'mfe1UserInput', description: 'Live text input from MFE 1' }
// ]Usage Examples
Example 1: Simple Tool Registration and Invocation
Host (Provider):
// packages/sandbox/src/components/tools/tools.tsx
import { useAtlasHandlerRegistry } from '@servicetitan/assist-utils';
const HostCommunication = () => {
const [counter, setCounter] = useState(0);
useAtlasHandlerRegistry('incrementCounter', {
name: 'Increment Counter',
description: 'Increments the counter in host',
parameters: [
{ name: 'amount', type: 'number', description: 'Amount to increment', required: true }
],
handler: ({ amount }) => {
setCounter(prev => prev + amount);
return { newCounter: counter + amount };
},
});
return <div>Counter: {counter}</div>;
};TI-Assist (Consumer):
// packages/ti-assist/src/modules/demo/demo.tsx
import { useAtlasToolInvoke } from '@servicetitan/assist-utils';
const Demo = () => {
const { invokeTool, availableTools } = useAtlasToolInvoke();
const handleIncrement = async () => {
const result = await invokeTool('incrementCounter', { amount: 5 });
console.log('New counter:', result.newCounter);
};
return (
<div>
<button onClick={handleIncrement}>Increment by 5</button>
<p>Available tools: {availableTools().length}</p>
</div>
);
};Example 2: Real-Time Data Sync
Host (Provider):
import { useAtlasContextRegister } from '@servicetitan/assist-utils';
const HostPanel = () => {
const [userInput, setUserInput] = useState('');
// Share user input in real-time
useAtlasContextRegister({
key: 'hostUserInput',
description: 'Live text input from host',
payload: userInput,
});
return (
<input
value={userInput}
onChange={e => setUserInput(e.target.value)}
placeholder="Type something..."
/>
);
};TI-Assist (Consumer):
import { useAtlasDataConsumer } from '@servicetitan/assist-utils';
const Demo = () => {
const hostUserInput = useAtlasDataConsumer<string>('hostUserInput');
return (
<div>
<h3>Host is typing:</h3>
<p>{hostUserInput || '(No input yet)'}</p>
<p>Characters: {hostUserInput?.length || 0}</p>
</div>
);
};Advanced Patterns
Pattern 1: Tool Chaining
Invoke multiple tools in sequence:
const { invokeTool } = useAtlasToolInvoke();
const handleComplexOperation = async () => {
// Step 1: Calculate total
const totalResult = await invokeTool('calculateTotal', {
price: 100,
taxRate: 0.15
});
// Step 2: Update status with result
await invokeTool('updateStatus', {
status: `Total calculated: $${totalResult.total}`
});
// Step 3: Increment counter
await invokeTool('incrementCounter', {
amount: 1
});
};Pattern 2: Aggregated Data Display
Combine data from multiple sources:
const hostState = useAtlasDataConsumer<{ counter: number; status: string }>('hostLiveState');
const mfeState = useAtlasDataConsumer<{ counter: number; status: string }>('mfe1LiveState');
const totalCounter = (hostState?.counter || 0) + (mfeState?.counter || 0);
return (
<div>
<h3>Combined Counter: {totalCounter}</h3>
<p>Host: {hostState?.counter || 0}</p>
<p>MFE: {mfeState?.counter || 0}</p>
</div>
);Pattern 3: Tool Discovery and Documentation
Build a dynamic tool explorer:
const { availableTools } = useAtlasToolInvoke();
return (
<div>
<h2>Available Tools</h2>
{availableTools().map(tool => (
<div key={tool.toolKey} style={{ border: '1px solid #ccc', padding: '10px' }}>
<h3>{tool.name}</h3>
<p>{tool.description}</p>
<h4>Parameters:</h4>
<ul>
{tool.parameters.map(param => (
<li key={param.name}>
<strong>{param.name}</strong> ({param.type})
{param.required && ' *required*'}
<br />
<em>{param.description}</em>
</li>
))}
</ul>
</div>
))}
</div>
);Best Practices
1. Tool Registration
✅ Do:
// Use descriptive tool keys
useAtlasHandlerRegistry('calculateTotalWithTax', { ... });
// Provide clear descriptions
description: 'Calculates the total price including tax based on the given tax rate'
// Validate parameters
handler: ({ price, taxRate }) => {
if (typeof price !== 'number' || price < 0) {
throw new Error('Price must be a positive number');
}
// ... rest of handler
}❌ Don't:
// Vague tool keys
useAtlasHandlerRegistry('calc', { ... });
// Missing descriptions
description: ''
// No validation
handler: ({ price, taxRate }) => price + (price * taxRate)2. Data Registry
✅ Do:
// Memoize data to prevent unnecessary updates
const sharedData = useMemo(
() => ({ counter, status }),
[counter, status]
);
useAtlasContextRegister({
key: 'myData',
payload: sharedData,
});
// Use specific keys
key: 'hostUserInputField'❌ Don't:
// Don't pass raw objects (causes re-renders)
useAtlasContextRegister({
key: 'myData',
payload: { counter, status }, // New object every render!
});
// Don't use generic keys
key: 'data'3. Error Handling
✅ Do:
// Wrap tool invocations in try-catch
try {
const result = await invokeTool('calculateTotal', { price, taxRate });
setResult(result);
} catch (error) {
console.error('Tool invocation failed:', error);
setError(error.message);
}4. Performance
✅ Do:
// Use useMemo for expensive computations
const expensiveData = useMemo(() => {
return computeExpensiveValue(input);
}, [input]);
useAtlasContextRegister({
key: 'expensiveData',
payload: expensiveData,
});
// Only re-register when necessary
useEffect(() => {
// ... registration logic
}, [dataKey, writer]); // Minimal dependencies5. Type Safety
✅ Do:
// Define interfaces for data
interface HostState {
counter: number;
status: string;
}
const hostState = useAtlasDataConsumer<HostState>('hostLiveState');
// Type tool parameters
interface CalculateTotalParams {
price: number;
taxRate: number;
}
handler: (params: CalculateTotalParams) => {
// TypeScript knows params.price is a number
}6. Naming Conventions
✅ Do:
// Tool keys: camelCase, descriptive verbs
'calculateTotal', 'updateUserProfile', 'fetchOrderDetails'
// Data keys: camelCase, source prefix
'hostUserInput', 'mfe1LiveState', 'dashboardMetrics'7. Documentation
✅ Do:
useAtlasHandlerRegistry('calculateShippingCost', {
name: 'Calculate Shipping Cost',
description: 'Calculates shipping cost based on weight, distance, and shipping method. Returns cost in USD.',
parameters: [
{
name: 'weight',
type: 'number',
description: 'Package weight in pounds',
required: true
},
{
name: 'distance',
type: 'number',
description: 'Shipping distance in miles',
required: true
},
{
name: 'method',
type: 'string',
description: 'Shipping method: "standard", "express", or "overnight"',
required: true
},
],
handler: ({ weight, distance, method }) => {
// Implementation
},
});Troubleshooting
Issue: Tools not showing up
Cause: Provider not properly configured or MFE not loaded
Solution:
- Host Application: Ensure you have wrapped your app with both
AtlasRegistryProviderandAtlasProvider
<AtlasRegistryProvider>
<AtlasProvider>
{/* Your app */}
</AtlasProvider>
</AtlasRegistryProvider>- MFE/TI-Assist: Ensure you have wrapped your app with
AtlasProvider
<AtlasProvider>
{/* Your MFE code */}
</AtlasProvider>- Check that the MFE providing the tool is actually loaded and mounted
Issue: Data not updating
Cause: Data reference not changing (object identity)
Solution:
// Use useMemo to ensure reference changes only when data changes
const sharedData = useMemo(
() => ({ counter, status }),
[counter, status]
);Issue: Infinite re-renders
Cause: Dependencies array causing effect to re-run continuously
Solution:
// Only include stable dependencies
useEffect(() => {
// registration logic
}, [componentKey, writer]); // Don't include objects/arrays unless memoizedIssue: Tool invocation timeout
Cause: Provider not running or tool handler not registered
Solution:
- Check console for
[Atlas Tool Registry] Registered tool: toolName - Ensure provider MFE is loaded
- Check tool key matches exactly (case-sensitive)
API Reference
Types
// Tool Configuration
interface FrontendToolParameter {
name: string;
type: string;
description: string;
required: boolean;
}
interface FrontendToolConfig {
assistantId?: string; // Optional: Associate tool with specific assistant
name: string;
description: string;
parameters: FrontendToolParameter[];
handler: (params: any) => Promise<any> | any;
}
// Data Configuration
interface AtlasContextConfig<T = any> {
assistantId?: string; // Optional: Associate data with specific assistant
key: string;
description?: string;
payload: T;
}Atlas EventBus contract
Event names and payload types for the shared host <-> Atlas EventBus. This package is the
source of truth: import them instead of re-declaring the ATLAS:* string literals, which is
how earlier copies drifted and lost fields.
import { events, SourceMessage } from '@servicetitan/assist-utils';
// Open the chat with a message and some context
eventBus.emit(events.openScreen, {
type: 'SimpleMessage',
text: 'Summarize this invoice',
context: { invoiceId: '123' },
fullscreen: true,
} satisfies SourceMessage);Exported: events, SourceMessage, AtlasMessageType, ChatAnchor, ChatPosition,
CHAT_ANCHORS, SetSuggestionsPayload, UserActionPayload, InteractionCreatedPayload,
InteractionEventPayload, createInteractionEventPayload.
AtlasMessageType is a string union rather than the generated MessageType enum from
@servicetitan/assist-api, which is workspace-private and so cannot be a dependency of a
published package. The union is derived from the exported ATLAS_MESSAGE_TYPES array, and
packages/ti-assist/src/utils/__tests__/events.test.ts fails if it ever drifts from the
generated enum.
Atlas's internalEvents (ATLAS:PUSH_MESSAGE, ATLAS:NEW_NOTIFICATION,
ATLAS:RESPONSE_ERROR, ATLAS:INTERACTION_CREATED) stay in ti-assist on purpose: they are
plumbing between Atlas's own stores, not a contract hosts should bind to.
Renaming any value here is a breaking change for every host on the bus, including ones outside this repo.
License
Internal ServiceTitan package
