Leviathan Page Capabilities
Give the Leviathan widget live tools to read, edit, save and drive a page's own data — the save/edit pattern every Leumas surface repeats.
Leviathan page capabilities — the save/edit pattern
Every Leumas product mounts the Leviathan widget (@leumas/leviathan) inside a CapabilityProvider (@leumas/capability-sdk). A page can register live tools with that provider; the widget sends them as function-call tools on every chat turn, executes returned clientToolCalls in the page via the registered handlers, and feeds the results back (up to 4 tool rounds per turn). The [fast] badge on the widget shows the live count. Navigate away → the scope unregisters → the tools vanish.
That loop is what turns Leviathan from "RAG over my data" into an operator of the page: it reads real state, mutates it through the page's own save path, and the user watches the UI update mid-conversation.
The one hook
import { useRegisterCapability } from '@leumas/capability-sdk';
useRegisterCapability([
{
name: 'add_statements', // snake_case verb
description: 'What it does + WHEN to use it', // the model reads this
parameters: { type: 'object', properties: { texts: { type: 'array', items: { type: 'string' } } }, required: ['texts'] },
call: async ({ texts }) => { // may be async; return JSON-able
await addStatements(texts, { source: 'ai' });
return { ok: true, added: texts.length };
},
},
], {
scopeId: 'graphs:editor', // 'domain:page' convention — stable, explicit
enabled: Boolean(doc), // gate until data is loaded
pageTitle: `Reasoning graph: ${doc?.name}`,
pageInstructions: 'Context + ground rules the bot gets while THIS page is open.',
});
Fresh array literals per render are fine — registration is keyed on the tools' shape (names/descriptions/parameters), and call always sees the latest closures. Unregister-on-unmount is automatic.
The save/edit rules (what makes the pattern solid)
- Tools call the page's EXISTING mutators — the exact functions the
buttons call (optimistic update → API verbs → refresh). Never write a parallel persistence path for the AI; then AI edits inherit every invariant, race guard and stats refresh for free, and are indistinguishable from hand edits.
- Read tools first, write tools second. Ship a
get_*_overviewthat
returns the page's real state compactly, and tell the bot (in pageInstructions) to call it before editing. Grounded reads are what make the edits sensible.
- Return small JSON, always
{ ok, ... }on writes — errors as
{ ok: false, error }, never throws the model can't read. Cap list sizes.
- "Show" tools drive the UI (select, focus, navigate) so the user sees
what the bot means. Selection/spotlight setters are capabilities too.
- Mark AI writes where the data model supports it (e.g. statements carry
source: 'ai') so provenance survives.
- One scope per page,
domain:pageid,enabled:gated on data readiness.
Keep 5–12 tools; more dilutes tool choice.
- Extract to a
use<Domain>Capabilities.jshook beside the page — the
page stays lean and the capability surface is reviewable in one file.
Reference implementations
- Canonical toy:
products/leumas-studio/src/admin/demos/DemoContacts.jsx
(add/find/tag contacts — the minimal shape).
- Real save/edit surface:
products/leumas-studio/src/admin/graphs/useGraphCapabilities.js
(reasoning graphs: overview/list/inspect reads + add/edit/delete/rename/mute writes + select/focus canvas drivers), registered in GraphEditor.jsx; the gallery registers a lighter list/open/create set in GraphsHome.jsx.
- SDK internals:
shared/packages/capability-sdk/src/{CapabilityProvider.jsx,useRegisterCapability.js};
widget tool loop: shared/packages/leviathan/src/Widget.jsx.
Checklist for a new surface
- Identify the page's mutators + state (the ones the UI already uses).
- Write
use<Domain>Capabilities.js: overview read → detail read →
writes-via-mutators → show tools. snake_case names, JSON-schema parameters, { ok } returns.
- Call it from the page component; scope
domain:page;enabledon data
readiness; pageInstructions = context + "read overview first" + any domain grammar (e.g. "statements are single declarative sentences").
- Verify: open the page → widget [fast] count rises → ask the bot to read, then to
make one small edit → the UI updates and the change persists on reload → navigate away → [fast] drops.
- Server-side twins: if the domain also has an adapter (functioncalls/MCP),
keep page tools for stateful, open-document work and adapter tools for stateless work — don't duplicate one as the other.