@sqlrooms/ai
v0.29.0
Published
High-level AI package for SQLRooms.
Readme
High-level AI package for SQLRooms.
This package combines:
- AI slice state/logic (
@sqlrooms/ai-core) - AI settings UI/state (
@sqlrooms/ai-settings) - AI config schemas (
@sqlrooms/ai-config) - SQL query and schema discovery tool helpers (
createDefaultAiTools,createQueryTool)
Use this package when you want AI chat + tool execution in a SQLRooms app without wiring low-level pieces manually.
createDefaultAiInstructions includes a hybrid DuckDB table context: small
current-database main catalogs include full schemas for every table, while
larger catalogs include a few full schemas, additional table names with row
counts, and instructions to call
read_table_schema before querying tables whose columns are not shown.
createDefaultAiTools registers list_tables and read_table_schema by
default so apps can expose the same table discovery workflow. These tools
search the current database main schema by default, and accept broader
schema, database, and pattern filters for other visible schemas or attached
databases.
createDefaultAiTools also registers command-layer tools when the room store
has a command registry:
search_commandsfor compact intent-based command discovery;get_commandfor full command metadata and input schema after selecting a command;execute_commandfor invoking the selected command;list_commandsfor broad command-registry debugging.
Model-facing flows should prefer
search_commands -> get_command (when input schema is needed) -> execute_command
instead of repeatedly listing the full command catalog. Keep schemas out of
search results by default; commands with requiresInput: false can run with
default input without a schema lookup. Search covers registered commands only;
other directly available AI tools should be called directly.
Search requires text relevance before applying resource/action hints or
availability bonuses. It ignores common filler words and matches query tokens
as words or command-ID segments. Read requests (get, read, list, show,
inspect) favor relevant read-only commands; exact command IDs retain priority.
Resource and action parameters remain ranking hints, while riskLevel is a
filter. Unmatched queries return zero commands; an empty query can still browse
the catalog. The reported match count is computed before the result limit.
execute_command refuses high-risk or
confirmation-required commands until the caller sets confirmed: true after an
explicit user confirmation. Skill runtimes can pass skillId, toolCallId,
traceId, and metadata through tool execution options; the command invocation
receives those fields for trace callbacks. When the current AI run has a
primary artifact context item, command tools also propagate it as the
invocation target. They read the mutable tool execution context first and fall
back to the invoking session's stored run context, never the visibly selected
chat. This keeps omitted artifact targets stable for the turn while allowing
set_primary_context_artifact to retarget later calls in the same turn.
DEFAULT_SKILL_RUNTIME_TOOL_POLICY
documents the default command, artifact-context, table/query, and high-level
agent tool policy for future skill runtimes. Hosts with product-specific agent
tool names can call createSkillRuntimeToolPolicy() to substitute names such
as their own block document agent while keeping the package defaults generic.
Hosts can scope a command tool instance with commandGuard. Denied descriptors
are omitted from search_commands, list_commands, and get_command, and
execute_command refuses them before validation, confirmation, or invocation.
The refusal uses command-not-available-to-caller unless the guard supplies a
custom code; a custom message can direct the model to an owning agent tool.
Direct store.commands.invokeCommand calls are unaffected.
const commandTools = createCommandTools(store, {
commandGuard: (descriptor) =>
descriptor.id.startsWith('block-document.') && !descriptor.readOnly
? {
allowed: false,
code: 'use-document-agent',
message: 'Use the document agent tool for document edits.',
}
: {allowed: true},
});Installation
npm install @sqlrooms/ai @sqlrooms/room-shell @sqlrooms/duckdb @sqlrooms/uiQuick start
import {
AiSettingsSliceState,
AiSliceState,
createAiSettingsSlice,
createAiSlice,
createDefaultAiInstructions,
createDefaultAiTools,
} from '@sqlrooms/ai';
import {
createRoomShellSlice,
createRoomStore,
RoomShellSliceState,
} from '@sqlrooms/room-shell';
type RoomState = RoomShellSliceState & AiSliceState & AiSettingsSliceState;
export const {roomStore, useRoomStore} = createRoomStore<RoomState>(
(set, get, store) => ({
...createRoomShellSlice({
config: {
dataSources: [
{
type: 'url',
tableName: 'earthquakes',
url: 'https://huggingface.co/datasets/sqlrooms/earthquakes/resolve/main/earthquakes.parquet',
},
],
},
})(set, get, store),
...createAiSettingsSlice()(set, get, store),
...createAiSlice({
tools: {
...createDefaultAiTools(store),
},
getInstructions: () => createDefaultAiInstructions(store),
// Optional: observe completed, non-aborted turns for app-owned behavior
// such as audit logging or analytics.
onChatFinish: ({sessionId, messages}) => {
void sessionId;
void messages;
},
})(set, get, store),
}),
);Render chat UI
import {Chat} from '@sqlrooms/ai';
import {useRoomStore} from './store';
function AiPanel() {
const updateProvider = useRoomStore(
(state) => state.aiSettings.updateProvider,
);
return (
<Chat>
<Chat.Sessions />
<Chat.Messages />
<Chat.PromptSuggestions>
<Chat.PromptSuggestions.Item text="Summarize the available tables" />
</Chat.PromptSuggestions>
<Chat.Composer placeholder="Ask a question about your data">
<Chat.InlineApiKeyInput
onSaveApiKey={(provider, apiKey) => {
updateProvider(provider, {apiKey});
}}
/>
<Chat.Composer.Attachments />
<Chat.ModelSelector />
</Chat.Composer>
</Chat>
);
}Chat.Composer.Attachments is opt-in. It accepts images plus plain-text and
Markdown files through explicit choices in the paperclip menu, shows removable
previews before sending, and renders posted attachments as clickable previews
that open in a larger dialog.
Customize chat presentation
Chat.Rendering accepts a partial set of presentation slots. Unspecified slots
keep the SQLRooms defaults, so an app can replace one region or row without
reimplementing the rest of the chat. ToolActivity is used for top-level and
nested tool rows; recursive agent progress and non-hoisted rich tool content
remain pre-wired when that row is customized.
import {
Chat,
type ChatActivityProps,
type ChatToolActivityProps,
} from '@sqlrooms/ai';
function AppActivity({children, isRunning}: ChatActivityProps) {
return <section aria-busy={isRunning}>{children}</section>;
}
function AppToolActivity({toolCall, isAgent}: ChatToolActivityProps) {
return (
<div>
{isAgent ? 'Agent' : 'Tool'}: {toolCall.toolName}
</div>
);
}
function AiMessages() {
return (
<Chat.Rendering
components={{
Activity: AppActivity,
ToolActivity: AppToolActivity,
}}
>
<Chat.Messages />
</Chat.Rendering>
);
}Use the Turn slot for a custom overall layout. Its semantic regions expose
pre-wired Content components, while activity items and action capabilities
remain available for deeper composition.
Block-scoped Ask AI actions
createAskAiBlockHeaderAction(...) builds a block-header actions renderer for
hosts that expose Ask AI on selected block types. The host controls the
supportsAiEditing policy and owns the submit flow; onSubmit receives the
block-document target context plus the submitted prompt. Pass the returned
renderer to the block-document chart/stateful renderer providers.
import {createAskAiBlockHeaderAction} from '@sqlrooms/ai';
const renderBlockHeaderActions = createAskAiBlockHeaderAction({
supportsAiEditing: (blockType) => ['chart', 'map'].includes(blockType),
onSubmit: (target, prompt) => {
void openBlockScopedChat({target, prompt});
},
});BlockAiPromptPopover is also re-exported for hosts that need a custom trigger
or placement. The public integration types are
AskAiBlockHeaderActionRenderContext, CreateAskAiBlockHeaderActionOptions,
and BlockAiPromptPopoverProps.
Generate Chat Titles
generateSessionTitle turns a session's early user messages into a concise title
via ai.sendPrompt, cleans the model output, and renames the session.
useGenerateSessionTitle wraps that helper for React surfaces that should watch
the current session and trigger title generation after new user messages. Apps
can keep product-specific policy outside the shared package by passing options
such as enabled, isDefaultSessionName, and getPromptOptions.
import {Chat, useGenerateSessionTitle} from '@sqlrooms/ai';
function AiPanel() {
useGenerateSessionTitle({
enabled: true,
getPromptOptions: () => ({useTools: false}),
});
return (
<Chat>
<Chat.Messages />
<Chat.Composer />
</Chat>
);
}Chat search
Chat renders a ChatSearchProvider and exposes Chat.Search, an in-conversation
find bar that highlights matches in the current session's messages.
For building search UIs outside the chat (e.g. a session list that searches across all sessions), the underlying matching primitives are re-exported and can be used without the provider:
normalizeChatSearchQuery(query)— trims + lower-cases a query (the casing rule the search uses).findChatSearchMatches(blocks, query)— returns positional matches (ChatSearchMatch[]) for a list ofChatSearchBlocks. Useful for highlighting matched substrings consistently withChat.Search.markdownToPlainText(markdown)— extracts plain text from markdown so message content can be made searchable.
import {findChatSearchMatches, type ChatSearchBlock} from '@sqlrooms/ai';
const blocks: ChatSearchBlock[] = [
{id: 'title', resultId: 'title', text: title},
];
const matches = findChatSearchMatches(blocks, query);Keeping highlighting in a replaced slot. A host that swaps out a chat leaf
slot renders its own text, so it loses the highlighting the default slot got for
free. HighlightedChatSearchText restores it. Pass the same blockId the turn
model registered for that part; without it there are no matches to highlight and
the component renders the text unchanged.
import {HighlightedChatSearchText} from '@sqlrooms/ai';
function AppPrompt({prompt, searchBlockId}: ChatPromptProps) {
return (
<MyPromptBubble>
<HighlightedChatSearchText text={prompt} blockId={searchBlockId} />
</MyPromptBubble>
);
}Matches are wrapped in <mark>, and the active match carries the match id as its
DOM id, so a host can scroll it into view. useOptionalChatSearch() exposes the
same state directly (activeMatchId, getMatchesForBlock) for slots that need
to do their own anchoring. It returns null outside a ChatSearchProvider, so a
component rendered away from Chat.Root degrades instead of throwing.
Indexing follows what actually rendered, not just what got registered. A slot
that returns null, or a region hidden behind a user preference, never mounts
HighlightedChatSearchText and so contributes no matches. Nothing is indexed
without something on screen to highlight. The text a slot renders is also the
text that gets indexed: a slot showing a transformed or shortened string is
searchable by what it actually displays, not by whatever text the block was
originally registered with.
Custom Markdown components are opaque rendering boundaries. Text beneath an
overridden Markdown element is excluded from automatic search because the
component may replace or hide its children; other default-rendered text in the
same message stays searchable. Overriding mark disables automatic search for
that message because generated highlights may never reach the DOM.
A slot that paints its own matches instead of rendering through
HighlightedChatSearchText must call useReportRenderedChatSearchBlock(blockId)
itself, or its block never counts as rendered and contributes no matches.
A slot that hides its content behind a disclosure or a toggle can key an effect
on useActiveChatSearchMatchKey(blockId) to reveal that content for every
selection attempt, including repeated navigation to the same match. This keeps
scrolling from landing on something still hidden and is what the default
reasoning disclosure uses to open itself.
Chat Session Types
Use ChatSessionSchema for persisted chat session validation and
isChatSessionEmpty for session emptiness checks. AnalysisSessionSchema,
AnalysisResultSchema, isAnalysisSessionEmpty, AnalysisResultsContainer,
and AnalysisResult remain compatibility exports for existing apps, but new
code should prefer Chat.Messages, uiMessages, and derived ChatTurn helpers
such as getChatTurnsFromUiMessages.
Old persisted sessions that contain analysisResults still load, but parsed and
new ChatSessionSchema state no longer includes that field.
Devtools
@sqlrooms/ai/devtools exposes development-oriented inspection components and
helpers without adding CodeMirror-heavy debug UI to the main @sqlrooms/ai
barrel.
import {ChatSessionDebugView} from '@sqlrooms/ai/devtools';
function DebugPanel({
sessionId,
onClose,
}: {
sessionId: string;
onClose?: () => void;
}) {
return <ChatSessionDebugView sessionId={sessionId} onClose={onClose} />;
}ChatSessionDebugView reads the existing AI store context and shows session
metadata, model selection, registered tools, run context, raw uiMessages, and
a tabbed chronological timeline that keeps message parts, tool calls, nested
agentProgress, optional agent snapshots, and copyable JSON blocks together.
Agent snapshot capture is opt-in on the AI slice:
createAiSlice({
tools,
getInstructions,
devtools: {
captureAgentSnapshots: true,
persistAgentSnapshots: true,
maxAgentSnapshotBytes: 64_000,
},
});Enable persistence when you need post-mortem or cross-tab debugging in saved workspace state. Snapshots are serializable metadata only; tool names, descriptions, capability flags, and approval hints may be stored, but implementations, closures, secrets, and unbounded prompt/output content should not be stored.
Add custom tools
import {tool} from 'ai';
import {z} from 'zod';
import {
createAiSlice,
createDefaultAiInstructions,
createDefaultAiTools,
} from '@sqlrooms/ai';
// inside createRoomStore(...):
createAiSlice({
tools: {
...createDefaultAiTools(store),
echo: tool({
description: 'Return user text back to the chat',
inputSchema: z.object({
text: z.string(),
}),
execute: async ({text}) => ({
success: true,
details: `Echo: ${text}`,
}),
}),
},
getInstructions: () => createDefaultAiInstructions(store),
})(set, get, store);Tool execute callbacks receive hidden run-context helpers in their second
argument. Apps can use getRunContext to capture selected artifacts at the
start of a run, expose them in formatRunContextInstructions, and then let
tools update the effective primary context with setPrimaryRunContextItem.
Old contexts without primaryItemId remain valid; the first item is treated as
primary. Artifact-specific context tools live in @sqlrooms/artifacts/ai.
Use remote endpoint mode
If you want server-side model calls, set chatEndPoint and optional chatHeaders:
// inside createRoomStore(...):
...createAiSlice({
tools: {
...createDefaultAiTools(store),
},
getInstructions: () => createDefaultAiInstructions(store),
chatEndPoint: '/api/chat',
chatHeaders: {
'x-app-name': 'my-sqlrooms-app',
},
})(set, get, store),Skills
The skills subsystem lets you define, store, and author reusable AI "skills" — named instruction sets that can be loaded into an agent at runtime.
Storage and types
SkillStorage is the interface that abstracts where skills live (filesystem, database, cloud, etc.). Implement it to plug in your own backend:
listRoots()— enumerate available skill root locationslistSkills(rootId)— list all skills under a rootreadSkill(ref)/writeSkill(ref, content)/deleteSkill(ref)— CRUD on individual skillsresolveSkillId(id)— resolve a bare id to its highest-prioritySkillRefsubscribe?(listener)— optional; subscribe to change notifications. Returns an unsubscribe function. Implementations that don't mutate (read-only/static) may omit this method.
Supporting types: SkillRoot, SkillManifest, SkillRef, SkillRecord, SkillListing, SkillWriteContent, SkillFile.
Composite storage
CompositeSkillStorage priority-merges multiple SkillStorage instances behind a single SkillStorage interface. Children are passed in priority order (highest first); they win conflicts in resolveSkillId and appear first in listRoots. Each child must own a unique set of rootIds.
subscribe fans out to every child that exposes the optional subscribe? method and aggregates the unsubscribes. If no child supports subscribe, composite.subscribe(...) is a noop returning a noop unsubscribe — consumers can call it unconditionally.
import {CompositeSkillStorage} from '@sqlrooms/ai';
// Higher-priority `userStorage` wins on id collisions; both contribute roots
// and listings to the merged view.
const storage = new CompositeSkillStorage([userStorage, builtInStorage]);
const roots = await storage.listRoots(); // [user roots..., built-in roots...]
const all = await storage.listSkills(); // union, with duplicates
// Optional change notification: composite forwards from any subscribe-capable
// child.
const unsubscribe = storage.subscribe(() => {
void refreshUi();
});
// later: unsubscribe();Manifest utilities
parseSkillManifest(raw)— parse and validate a skill manifest (Zod-backed, throwsSkillManifestErroron failure)serializeSkillManifest(manifest)— serialize a manifest back to its raw formloadSkillFromFiles(files)— assemble aSkillRecordfrom a set ofSkillFileobjects (manifest + instruction body)
Error types
All skill errors extend SkillError and carry a typed SkillErrorCode:
| Class | When thrown |
| ------------------------ | ----------------------------------- |
| SkillManifestError | Manifest parse/validation failure |
| SkillNotFoundError | Skill ref does not exist in storage |
| SkillRootReadOnlyError | Write attempted on a read-only root |
| SkillConflictError | Skill ID collision on write |
Skill authoring
A built-in agent-driven authoring flow that generates skill content through a conversational UI:
createSkillAuthoringAgent(options)— construct aToolLoopAgentscoped to skill creation; acceptsCreateSkillAuthoringAgentOptionscreateSkillDraftStore()— Zustand store for tracking the in-progress draft (SkillDraftStore,SkillDraftState)SkillAuthoringPanel— drop-in panel component that wiresChat.LocalAgentRootto the authoring agent; acceptsSkillAuthoringPanelPropsSkillDraftPreview— read-only preview of the current draft manifest and instructions; acceptsSkillDraftPreviewPropsDefaultSkillAuthoringPanelHeader— default header forSkillAuthoringPanel
Types: SkillAuthoringContext, SkillDraft, SkillDraftStatus, SaveSkillCallback, CreateSkillAuthoringAgentOptions.
Lower-level authoring tools (exported for advanced use): createWriteManifestTool, createWriteInstructionsTool, createSaveSkillTool, buildSkillAuthoringSystemPrompt, containsForbidden, DEFAULT_SKILL_AUTHORING_STOP_STEPS.
import {
createSkillAuthoringAgent,
createSkillDraftStore,
SkillAuthoringPanel,
} from '@sqlrooms/ai';
const draftStore = createSkillDraftStore();
const agent = createSkillAuthoringAgent({
model: myLanguageModel,
draftStore,
onSave: async (skill) => {
await mySkillStorage.writeSkill(
{rootId: 'default', skillId: skill.id},
skill,
);
},
});
function SkillCreator() {
return <SkillAuthoringPanel agent={agent} draftStore={draftStore} />;
}Related packages
@sqlrooms/ai-corefor lower-level AI slice and chat primitives@sqlrooms/ai-settingsfor settings slice/components only@sqlrooms/ai-configfor Zod schemas and migrations
