Hooks#
useAui()#
function useAui(): AssistantClient;
Returns the store from the nearest AuiProvider. The client is immutable: state updates keep its identity, while a structural change (a scope resolving to a different instance) produces a new client and re-renders consumers.
AuiConfig#
function AuiConfig(config: AuiConfig.Input): AuiConfig;
type AuiConfig.Input = {
[K in ClientNames]?: ClientElement<K> | DerivedElement<K>;
};
Builds the branded config object accepted by AuiProvider's config prop. Raw object literals are a type error.
The useAui(scopes) extension overload is deprecated; hoist const aui = useAui() and const config = AuiConfig(scopes) and use <AuiProvider extends={aui} config={config}> instead.
useAuiState#
function useAuiState<T>(selector: (state: AssistantState) => T): T;
Subscribes to a slice of state. Re-renders only when the selected value changes (compared by Object.is). The selector must return a specific value — not the entire state object. Scopes that may be unavailable can be read via s.optional.<scope>, which resolves to undefined instead of throwing.
useAuiEvent#
function useAuiEvent<TEvent extends AssistantEventName>(
selector: AssistantEventSelector<TEvent>,
callback: AssistantEventCallback<TEvent>,
): void;
Subscribes to events. The selector can be a string ("scope.event") or an object ({ scope, event }). Unsubscribes on unmount.
Components#
AuiProvider#
const aui = useAui();
const config = AuiConfig({ counter: CounterResource() });
// Top-level root
<AuiProvider config={config}>{children}</AuiProvider>
// Nested: extend the parent (or extends={null} to isolate)
<AuiProvider extends={aui} config={config}>{children}</AuiProvider>
Provides an AssistantClient to the React tree, created from a config built with AuiConfig(...). Nested under a parent provider, extends is mandatory: extends={aui} extends the parent, extends={null} isolates. An empty config creates a client extending the extends client. ref receives the resulting client after mount. Child components access the client via useAui().
AuiIf#
<AuiIf condition={(s) => s.counter.count > 0}>
<ResetButton />
</AuiIf>
Renders children only when the condition returns true. Uses useAuiState internally.
RenderChildrenWithAccessor#
<RenderChildrenWithAccessor
getItemState={(aui) => aui.todoList.todo({ index }).getState()}
{(getItem) =>
children({
get todo() {
return getItem();
},
})
}
</RenderChildrenWithAccessor>
Sets up a lazy item accessor for list rendering. The getItem function defers reading state until the consumer accesses it — if the children render function never reads the item, no subscription is created. When children returns a propless component (e.g. {() => <Todo />}), the output is automatically memoized.
| Prop | Type |
|---|---|
getItemState | (aui: AssistantClient) => T |
children | (getItem: () => T) => ReactNode |
See Rendering Lists for the full pattern.
Resource utilities#
Derived#
function Derived<K extends ClientNames>(config: Derived.Props<K>): DerivedElement<K>;
Creates a derived scope that points to data in a parent scope.
Derived({
source: "thread",
query: { index: 0 },
get: (aui) => aui.thread.message({ index: 0 }),
});
The get function receives the current AssistantClient and must return a client created via useClientResource (or useClientLookup/useClientList) — typically the result of calling a parent scope method. The meta (source, query) acts as identity: when query changes between renders, a new accessor is returned in the same render pass — useful for keying child consumers like MessageByIndex by index.
attachTransformScopes#
function attachTransformScopes(
hook: (...args: any[]) => any,
transform: (scopes: ScopesConfig, parent: AssistantClient) => void,
): void;
Attaches a transform function to a resource hook — pass the hook function you gave to resource(), not the resource wrapper. When the resource is mounted via AuiProvider, the transform runs and can add or modify sibling scopes by mutating scopes. Transforms are applied iteratively — new root scopes trigger their own transforms.
One transform per hook. Throws on duplicate.
forwardTransformScopes#
function forwardTransformScopes(
target: (...args: any[]) => any,
source: (...args: any[]) => any,
): void;
Copies source's transform onto target, composing with any transform target already has. Use when one resource hook wraps another.
ScopesConfig#
type ScopesConfig = {
[K in ClientNames]?: ClientElement<K> | DerivedElement<K>;
};
The scopes object passed to AuiConfig and mutated by transforms.
Resource hooks#
These are used inside Tap resources to integrate with Store.
useClientResource#
function useClientResource<TMethods extends ClientMethods>(
element: ResourceElement<TMethods>,
): {
state: InferClientState<TMethods>;
methods: TMethods;
key: string | number | undefined;
};
Wraps a single resource element into a client. Adds the client to the internal client stack for event scoping.
state is inferred from the element's getState() return type. If getState is not defined, state is undefined.
useClientLookup#
function useClientLookup<TMethods extends ClientMethods>(
elements: readonly ResourceElement<TMethods>[],
): {
state: InferClientState<TMethods>[];
get: (lookup: { index: number } | { key: string }) => TMethods;
};
Wraps a list of resource elements into clients. Each element must have a key (via withKey). Uses useClientResource internally for each element.
get resolves a client by index or key. Throws if the lookup doesn't match.
useClientList#
function useClientList<TData, TMethods extends ClientMethods>(
props: useClientList.Props<TData, TMethods>,
): {
state: InferClientState<TMethods>[];
get: (lookup: { index: number } | { key: string }) => TMethods;
add: (data: TData) => void;
};
Manages a dynamic list of clients with add/remove. Built on useClientLookup.
type useClientList.Props<TData, TMethods> = {
initialValues: TData[];
getKey: (data: TData) => string;
resource: Resource<TMethods, [useClientList.ResourceProps<TData>]>;
};
type useClientList.ResourceProps<TData> = {
key: string;
getInitialData: () => TData;
remove: () => void;
};
getInitialData() is called once on mount. remove() removes the item from the list. Throws on duplicate key.
useAssistantClientRef#
function useAssistantClientRef(): {
parent: AssistantClient;
current: AssistantClient | null;
};
Returns a ref to the store being built. current is null during resource creation and populated after all sibling scopes are mounted. Use in useEffect to access sibling scopes at runtime.
useAssistantEmit#
function useAssistantEmit(): <TEvent extends Exclude<AssistantEventName, "*">>(
event: TEvent,
payload: AssistantEventPayload[TEvent],
) => void;
Returns a stable emit function. Events are delivered via microtask — listeners fire after the current state update settles.
getClientId#
function getClientId(client: object): getClientId.ClientId;
Returns the opaque identity of a bound client instance. Stable for the lifetime of the bound client regardless of accessor wrapping — a reliable WeakMap key for per-client caches. Throws for an unavailable scope's accessor.
Types#
ScopeRegistry#
interface ScopeRegistry {}
Module augmentation point. Augment this interface to register scopes:
declare module "@assistant-ui/store" {
interface ScopeRegistry {
counter: {
methods: {
getState: () => { count: number };
increment: () => void;
};
meta?: { source: ClientNames; query: Record<string, unknown> };
events?: { "counter.incremented": { newCount: number } };
};
}
}
methods is required. meta and events are optional.
ClientOutput#
type ClientOutput<K extends ClientNames> = ClientSchemas[K]["methods"] & ClientMethods;
The return type for a resource implementing scope K. Use as the return type annotation on your resource function.
ClientNames#
type ClientNames = keyof ClientSchemas;
Union of all registered scope names.
AssistantClient#
type AssistantClient = {
[K in ClientNames]: AssistantClientAccessor<K>;
} & {
readonly optional: {
readonly [K in ClientNames]: AssistantClientAccessor<K> | undefined;
};
subscribe(listener: () => void): Unsubscribe;
on<TEvent extends AssistantEventName>(
selector: AssistantEventSelector<TEvent>,
callback: AssistantEventCallback<TEvent>,
): Unsubscribe;
};
The store object returned by useAui(). Each scope is an accessor. optional exposes the same scopes, resolving an unavailable one to undefined instead of a throwing accessor: aui.optional.counter?.increment(). subscribe fires on any state change. on subscribes to typed events.
AssistantClientAccessor#
type AssistantClientAccessor<K extends ClientNames> =
ClientSchemas[K]["methods"] & {
/** @deprecated Access the scope as a property instead. */
(): ClientSchemas[K]["methods"];
} & (
| ClientMeta<K>
| { source: "root"; query: Record<string, never> }
| { source: null; query: null }
) & { name: K };
A scope accessor exposing the scope's methods as properties (aui.counter.increment()). Read .source, .query, and .name for metadata; check availability via aui.optional.counter. The call signature (aui.counter()) is deprecated and returns the same accessor.
AssistantState#
type AssistantState = ScopeStates & {
readonly optional: {
readonly [K in keyof ScopeStates]: ScopeStates[K] | undefined;
};
};
type ScopeStates = {
[K in ClientNames]: ClientSchemas[K]["methods"] extends {
getState: () => infer S;
}
? S
: never;
};
The state object passed to useAuiState selectors. Each key is the return type of that scope's getState(). optional exposes the same scopes, but an unavailable scope resolves to undefined instead of throwing: s.optional.counter?.count.
ClientMeta#
type ClientMeta<K extends ClientNames> =
"meta" extends keyof ClientSchemas[K]
? Pick<ClientSchemas[K]["meta"], "source" | "query">
: never;
The source and query shape for scope K, if meta is declared in ScopeRegistry.
ClientElement#
type ClientElement<K extends ClientNames> = ResourceElement<ClientOutput<K>>;
A resource element that implements scope K.
DerivedElement#
type DerivedElement<K extends ClientNames> =
ResourceElement<ReturnType<AssistantClientAccessor<K>>>;
The element returned by Derived(...) for scope K.
ClientMethods#
interface ClientMethods {
[key: string | symbol]: (...args: any[]) => any;
}
Base constraint for a scope's methods.
ClientSchema#
type ClientSchema<
TMethods extends ClientMethods = ClientMethods,
TMeta extends { source: ClientNames; query: Record<string, unknown> } = never,
TEvents extends Record<string, unknown> = never,
> = {
methods: TMethods;
meta?: TMeta;
events?: TEvents;
};
Helper for declaring a ScopeRegistry entry.
ClientEvents#
type ClientEvents<K extends ClientNames> =
"events" extends keyof ClientSchemas[K]
? ClientSchemas[K]["events"] extends Record<`${K}.${string}`, unknown>
? ClientSchemas[K]["events"]
: never
: never;
The event map declared by scope K, if any.
Unsubscribe#
type Unsubscribe = () => void;
Event types#
AssistantEventName#
type AssistantEventName = keyof AssistantEventPayload;
Union of all registered event names, plus "*".
AssistantEventPayload#
type AssistantEventPayload = ClientEventMap & {
"*": { [K in keyof ClientEventMap]: { event: K; payload: ClientEventMap[K] } }[keyof ClientEventMap];
};
Maps event names to their payload types. The "*" key receives a wrapped { event, payload } object.
AssistantEventSelector#
type AssistantEventSelector<TEvent extends AssistantEventName> =
| TEvent
| { scope: AssistantEventScope<TEvent>; event: TEvent };
A string ("scope.event") or object ({ scope, event }). Strings default to scope matching the event's source.
AssistantEventScope#
type AssistantEventScope<TEvent extends AssistantEventName> =
| "*"
| EventSource<TEvent>
| AncestorsOf<EventSource<TEvent>>;
Valid scopes to listen at: the event's source scope, any ancestor of that scope, or "*" for all.
AssistantEventCallback#
type AssistantEventCallback<TEvent extends AssistantEventName> = (
payload: AssistantEventPayload[TEvent],
) => void;
normalizeEventSelector#
function normalizeEventSelector<TEvent extends AssistantEventName>(
selector: AssistantEventSelector<TEvent>,
): { scope: AssistantEventScope<TEvent>; event: TEvent };
Converts a string selector to { scope, event } form. Strings like "counter.incremented" become { scope: "counter", event: "counter.incremented" }.