LuxoAILuxoAIdocs
Welcome
InvoiceAgentInternalSupplyAgentBetaLearnAgentBeta
OverviewAgent Corev0.11.0StableObservabilityv0.6.0StableLuxo Assistantv0.6.0BetaFeedback UIv0.2.0BetaLuxo MLScaffold
API Console
All systemsv0.8.1
LuxoAILuxoAIdocs
WelcomeApiLuxo Assistant

@luxoai-dev/luxo-assistant · API Reference

Luxo Assistant.

Betav0.6.0

Server: a route factory that handles auth, persistence, quota, completion (buffered or SSE), feedback persistence, and directive parsing. Client: a single LuxoRoot that renders the orb, drawer, streaming messages, inline feedback, and voice mode. Both share a LuxoChatManifest — that’s the contract.

At a glance
Package
@luxoai-dev/luxo-assistant
Version
v0.6.0
Modules
. (client) · /server · /styles.css
Peers
react 18+ · next 15+ · @luxoai-dev/agent-core 0.10+
Pairs with
agent-core

Server route · /server

One factory builds a Next.js { GET, POST, DELETE } route group that handles auth, persistence, quota, completion, and directive parsing. Use this for app/api/luxo/chat/route.ts.

import { ... } from "@luxoai-dev/luxo-assistant/server";
createLuxoChatRoutecreateEnvLuxoCompletecreateEnvLuxoStreamCompletecreateLuxoFeedbackRoutecreateLuxoActionBeaconRoutecreateSupabaseLuxoPersistencecreateLuxoAutoTitlerLuxoRouteOptionsLuxoChatManifestLuxoSurfaceLuxoCompleteInputLuxoCompleteOutputLuxoCompleteLuxoStreamCompleteLuxoStreamChunkLuxoPersistenceAdapterLuxoQuotaAdapterLuxoI18nDictionary
Function

createLuxoChatRoute

Beta
function createLuxoChatRoute(
  options: LuxoRouteOptions,
): {
  GET: (request: Request) => Promise<Response>;
  POST: (request: Request) => Promise<Response>;
  DELETE: (request: Request) => Promise<Response>;
}
Build a complete chat route in one call. The factory handles same-origin checks, optional auth, optional persistence, optional quota, prompt construction, completion (defaults to createEnvLuxoComplete), and directive parsing. Re-export GET / POST / DELETE from your route.ts.
Parameters
ParamTypeRequiredDescription
options.manifestLuxoChatManifestRequiredProduct identity, surfaces, actions, starter prompts, model routes, and (v0.2+) i18n dictionary for every user-facing string the route emits.
options.authenticate(req) => Promise<LuxoRouteAuthResult | null>OptionalResolve the actor from the request. Return null to deny. Required when requireAuth is true (default).
options.requireAuthbooleanOptionalDefaults to true. When false, anonymous calls are allowed and a synthetic tenantId is used.
options.completeLuxoCompleteOptionalBuffered completion. Defaults to createEnvLuxoComplete using the manifest's model routes and process env keys.
options.streamCompleteLuxoStreamCompleteOptionalAdded in v0.3.0. When provided AND the client sends 'Accept: text/event-stream', the route returns a ReadableStream of SSE events (start / text / done / error). Set both complete and streamComplete to let clients choose by Accept header.
options.persistenceLuxoPersistenceAdapterOptionalHook to load/save conversations and messages. Each method is independently optional. Use createSupabaseLuxoPersistence for the reference Supabase shape.
options.quotaLuxoQuotaAdapterOptionalPer-actor quota check. Return { allowed: false } to surface a 429 with quota details. The client also receives this snapshot via the optional 'quota' prop to gate sends pre-flight.
options.onGatewayEvent(event) => void | Promise<void>OptionalForwarded to the underlying gateway when using createEnvLuxoComplete / createEnvLuxoStreamComplete.
options.onActionsProposed(ctx) => void | Promise<void>OptionalAdded in v0.7.0. Fires after parseLuxoResponseDirectives extracts `!>` action directives from the assistant response. Receives { actor, actions, conversationId, pathname, requestId, surfaceId }. Wire to recordLuxoAction({ status: "proposed" }) from @luxoai-dev/observability. Best-effort: errors are logged and swallowed.
options.siteUrlstring | () => string | undefinedOptionalTrusted origin for same-origin checks. Defaults to NEXT_PUBLIC_SITE_URL / SITE_URL / DOMAIN_URL.

Returns

{ GET, POST, DELETE }

All three Web Response handlers.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// app/api/luxo/chat/route.ts
import { createLuxoChatRoute } from "@luxoai-dev/luxo-assistant/server";
import { getActorFromRequest } from "@/lib/auth";

const chat = createLuxoChatRoute({
  manifest: {
    serviceId: "support-assistant",
    productName: "Support Inbox",
    audience: "support agents",
    domain: "customer support",
    defaultSurface: {
      key: "ticket",
      label: "Ticket triage",
      shortLabel: "Triage",
      accentLabel: "Support",
      summary: "Triage a ticket and recommend the next safest step.",
      tooltip: "Ask Luxo about this ticket",
      paths: ["/tickets"],
    },
  },
  authenticate: async (req) => {
    const actor = await getActorFromRequest(req);
    return actor ? { actor } : null;
  },
});

export const GET = chat.GET;
export const POST = chat.POST;
export const DELETE = chat.DELETE;
Function

createEnvLuxoComplete

Beta
function createEnvLuxoComplete(
  manifest: LuxoChatManifest,
  onGatewayEvent?: (event: LLMGatewayEvent) => void | Promise<void>,
): LuxoComplete
Default completion. Reads GEMINI_API_KEY and DEEPSEEK_API_KEY from process env. Routes free chat to Gemini (or DeepSeek as a fallback), pro chat to DeepSeek. Throws GatewayError when neither key is present. Use this unless you need a custom provider mix — then implement LuxoComplete yourself and pass via options.complete.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Custom routing — bypass createEnvLuxoComplete
import { createLuxoGateway } from "@luxoai-dev/agent-core/llm";
import { anthropicAdapter } from "@luxoai-dev/agent-core/adapters/anthropic";

const gateway = createLuxoGateway({
  adapters: { anthropic: anthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY! }) },
  routes: [
    { feature: "support.luxo.chat.free", provider: "anthropic", model: "claude-haiku-4-5-20251001" },
    { feature: "support.luxo.chat.pro",  provider: "anthropic", model: "claude-opus-4-7" },
  ],
});

const chat = createLuxoChatRoute({
  manifest,
  complete: async (input) => {
    const feature = input.actor.plan === "pro"
      ? "support.luxo.chat.pro"
      : "support.luxo.chat.free";
    const response = await gateway.complete({
      feature,
      actor: input.actor,
      messages: [
        { role: "system", content: input.system },
        ...input.messages.map((m) => ({ role: m.role, content: m.text })),
      ],
      maxTokens: 2500,
      temperature: 0.45,
    });
    return {
      text: response.text,
      modelUsed: response.modelUsed,
      providerUsed: response.providerUsed,
      requestId: response.requestId,
      raw: response.raw,
    };
  },
});
Function

createEnvLuxoStreamComplete

since v0.3Beta
function createEnvLuxoStreamComplete(
  manifest: LuxoChatManifest,
  onGatewayEvent?: (event: LLMGatewayEvent) => void | Promise<void>,
): LuxoStreamComplete
Streaming sibling of createEnvLuxoComplete. Returns a LuxoStreamComplete that calls gateway.completeStream under the hood. Shares the env-resolved gateway with the buffered helper so consumers can wire both in one block: complete: createEnvLuxoComplete(...), streamComplete: createEnvLuxoStreamComplete(...).
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/api/luxo/chat/route.ts (streaming + buffered fallback)
import {
  createLuxoChatRoute,
  createEnvLuxoComplete,
  createEnvLuxoStreamComplete,
} from "@luxoai-dev/luxo-assistant/server";
import { manifest } from "@/lib/luxo/manifest";

const chat = createLuxoChatRoute({
  manifest,
  authenticate,
  complete: createEnvLuxoComplete(manifest, onGatewayEvent),
  streamComplete: createEnvLuxoStreamComplete(manifest, onGatewayEvent),
});

export const GET = chat.GET;
export const POST = chat.POST;
export const DELETE = chat.DELETE;
Function

createLuxoFeedbackRoute

since v0.4Beta
function createLuxoFeedbackRoute(
  options: CreateLuxoFeedbackRouteOptions,
): { POST: (request: Request) => Promise<Response> }
Thin Luxo-aware wrapper around createFeedbackRoute from @luxoai-dev/agent-core/feedback. Fills in Luxo defaults (table luxo_feedback, feature label {serviceId}.luxo.chat) so wiring per-message thumbs/stars feedback is a one-line route handler. Wire onRecorded to recordFeedback from @luxoai-dev/observability to mirror chat ratings into Prometheus and structured logs.
Parameters
ParamTypeRequiredDescription
options.authenticate(req) => Promise<{ actor: LuxoActor } | null>RequiredResolve the actor for the feedback POST.
options.supabaseSupabaseLikeClientRequiredSupabase admin client used to insert the row.
options.manifestLuxoChatManifestRequiredUsed to derive the feature label and tenant metadata.
options.tablestringOptionalOverride the table. Defaults to 'luxo_feedback'.
options.onRecorded(entry: FeedbackEntry) => void | Promise<void>OptionalSide-effect after the row is written — pair with recordFeedback for metrics.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/api/luxo/feedback/route.ts
import { createLuxoFeedbackRoute } from "@luxoai-dev/luxo-assistant/server";
import { recordFeedback } from "@luxoai-dev/observability";
import { manifest } from "@/lib/luxo/manifest";
import { admin } from "@/lib/supabase/admin";
import { getActorFromRequest } from "@/lib/auth";

const feedback = createLuxoFeedbackRoute({
  authenticate: async (req) => {
    const actor = await getActorFromRequest(req);
    return actor ? { actor } : null;
  },
  supabase: admin,
  manifest,
  onRecorded: (entry) =>
    recordFeedback({
      service: manifest.serviceId,
      feature: `${manifest.serviceId}.luxo.chat`,
      score: entry.score,
    }),
});

export const POST = feedback.POST;

Reference Supabase migration

The package ships a reference schema at @xdanielsb/luxo-assistant/src/luxo-feedback-schema.sql — apply once per Supabase project. Includes RLS policies keyed on auth.uid()::text = user_id plus explicit grant ... to authenticated (added in v0.7.0 — without it cookie-based SSR clients hit permission denied for table luxo_feedback before RLS is reached).
Function

createLuxoActionBeaconRoute

since v0.7Beta
function createLuxoActionBeaconRoute(
  options: LuxoActionBeaconRouteOptions,
): { POST: (request: Request) => Promise<Response> }
Endpoint that receives navigator.sendBeacon POSTs from the chat client every time a user actually clicks an action chip. Pair with actionBeaconEndpoint on LuxoRoot — the client beacons { actionId, surface?, conversationId? } and this route dispatches your onRecorded handler. Wire it to recordLuxoAction from @luxoai-dev/observability with status: "executed". Always responds 204 — beacons never block the user.
Parameters
ParamTypeRequiredDescription
options.authenticate(req) => Promise<LuxoActionBeaconAuth | null>OptionalResolve the actor for the beacon. When requireAuth (default true) is set, returning null produces 401.
options.requireAuthbooleanOptionalDefaults to true. Set false for unauthenticated public-surface beacons.
options.onRecorded({ auth, payload, request }) => void | Promise<void>RequiredSide-effect when the beacon arrives. Pair with recordLuxoAction({ status: "executed" }) for Prometheus counters.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// app/api/luxo/action/route.ts
import { createLuxoActionBeaconRoute } from "@luxoai-dev/luxo-assistant/server";
import { recordLuxoAction } from "@luxoai-dev/observability";
import { getActorFromRequest } from "@/lib/auth";

const beacon = createLuxoActionBeaconRoute({
  authenticate: async (req) => {
    const actor = await getActorFromRequest(req);
    return actor ? { userId: actor.userId, tenantId: actor.tenantId } : null;
  },
  onRecorded: ({ auth, payload }) => {
    recordLuxoAction({
      actionId: payload.actionId,
      status: "executed",
      surface: payload.surface,
      conversationId: payload.conversationId,
      serviceName: "supplyagent",
      userId: auth?.userId,
    });
  },
});

export const POST = beacon.POST;
Function

createSupabaseLuxoPersistence

since v0.2Beta
function createSupabaseLuxoPersistence(
  options: SupabaseLuxoPersistenceOptions,
): LuxoPersistenceAdapter
Reference Supabase-backed implementation of LuxoPersistenceAdapter. Default tables luxo_conversations and luxo_messages; reference schema shipped as persistence-schema.sql. Removes ~100 lines of CRUD per consumer.
Parameters
ParamTypeRequiredDescription
options.clientSupabaseLikeClientRequiredSupabase admin client.
options.conversationsTablestringOptionalOverride default 'luxo_conversations'.
options.messagesTablestringOptionalOverride default 'luxo_messages'.

Returns

LuxoPersistenceAdapter

Function

createLuxoAutoTitler

since v0.2Beta
function createLuxoAutoTitler(
  options: CreateLuxoAutoTitlerOptions,
): LuxoAutoTitler
Generates a sanitized 3–6 word title for a conversation by asking the LLM to summarize the opening exchange. Promoted from a single consumer that had implemented it locally; now shared. Re-exported from both @luxoai-dev/luxo-assistant (main) and /server.
interface

LuxoChatManifest

interface LuxoChatManifest {
  serviceId: string;
  productName: string;
  audience: string;
  domain: string;
  defaultSurface: LuxoSurface;
  surfaces?: readonly LuxoSurface[];
  actions?: readonly LuxoActionDefinition[];
  guardrails?: readonly string[];
  starterPrompts?: readonly string[];
  systemInstructions?: readonly string[];
  assistantName?: string;
  model?: LuxoModelRoutes;
  i18n?: LuxoI18nDictionary;            // v0.2+
}
The single contract that ties server + client together. Pass the same manifest to createLuxoChatRoute on the server and LuxoRoot on the client.
Fields
FieldTypeRequiredDescription
serviceIdstringRequiredStable id used for tenant scoping and metric labels (e.g. 'support-assistant').
productNamestringRequiredHuman-friendly product name shown in the UI.
audiencestringRequiredOne-line description of who's using the assistant. Goes into the system prompt.
domainstringRequiredApplication domain (e.g. 'customer support').
defaultSurfaceLuxoSurfaceRequiredSurface used when no surface matches the current pathname.
surfacesreadonly LuxoSurface[]OptionalAdditional surfaces matched by path or function.
actionsreadonly LuxoActionDefinition[]OptionalExecutable actions the assistant can propose via !> directives.
guardrailsreadonly string[]OptionalFree-form guardrail lines injected into the system prompt.
starterPromptsreadonly string[]OptionalSuggestions shown in an empty chat.
systemInstructionsreadonly string[]OptionalAdditional system lines (e.g. tone, voice, brand).
assistantNamestringOptionalOverride the default 'Luxo' identity.
modelLuxoModelRoutesOptionalOverride env keys / model names / feature names used by createEnvLuxoComplete.
i18nLuxoI18nDictionaryOptionalAdded in v0.2.0. Translation dictionary for every user-facing string emitted by createLuxoChatRoute (auth required, quota exceeded, invalid request body/origin/url, need user message, provider not configured / unreachable, opening-action fallback). Populate from next-intl, lingui, etc. English defaults preserved when keys are absent.
interface

LuxoSurface

interface LuxoSurface {
  key: string;
  label: string;
  shortLabel: string;
  accentLabel: string;
  summary: string;
  tooltip: string;
  paths?: readonly string[];
  match?: (pathname: string) => boolean;
  starterPrompts?: readonly string[];
  palette?: Partial<LuxoPalette>;
  motion?: Partial<LuxoMotionProfile>;
  scripts?: Partial<Record<LuxoBuiltinActionId, readonly string[]>>;
}
A surface is a context the assistant knows about (e.g. 'reviewing a ticket'). Surfaces are resolved per-request from the URL pathname; the resolved surface is passed into prompt construction.
Fields
FieldTypeRequiredDescription
keystringRequiredStable id (e.g. 'ticket', 'review', 'inbox').
labelstringRequiredHuman label.
shortLabelstringRequiredCompact label for chips.
accentLabelstringRequiredCategory label (e.g. 'Support', 'Finance').
summarystringRequiredOne-line description used in the system prompt.
tooltipstringRequiredHover hint on the orb.
pathsreadonly string[]OptionalGlob-like path matchers for resolution.
match(pathname) => booleanOptionalCustom matcher when paths can't express it.
starterPromptsreadonly string[]OptionalSurface-specific starter prompts.
interface

LuxoCompleteInput

interface LuxoCompleteInput {
  actor: LuxoActor;
  messages: LuxoChatMessage[];
  pageTitle?: string | null;
  pathname: string;
  surface: ResolvedLuxoSurface;
  surfaceContext?: Record<string, unknown>;
  system: string;
}
Everything the completer receives. The system string is already built — concatenate to messages and call your provider.
interface

LuxoCompleteOutput

interface LuxoCompleteOutput {
  text: string;
  modelUsed?: string;
  providerUsed?: string;
  requestId?: string;
  raw?: unknown;
}
Return from a custom completer. The route handler parses directives from text, persists the assistant message, and replies.
interface

LuxoActor

interface LuxoActor extends ActorContext {
  displayName?: string;
  email?: string;
}
ActorContext plus optional UI fields.
interface

LuxoChatMessage

interface LuxoChatMessage {
  id: string;
  role: "user" | "assistant";
  text: string;
  llmRequestId?: string;
  modelUsed?: string;
  providerUsed?: string;
}
In-conversation message. Persistence adapters return arrays of these on load.
interface

LuxoActionDefinition

interface LuxoActionDefinition {
  id: string;
  label: string;
  description: string;
  risk: "safe" | "confirm" | "restricted";
  kind?: "navigate" | "event" | "none";
  availability?: "public" | "authenticated" | "pro" | "admin";
  requiresConfirmation?: boolean;
  surface?: string | readonly string[];
  params?: Readonly<Record<string, LuxoActionParamDefinition>>;
  build?: (params: Record<string, string>) => LuxoExecutableAction | null;
  aliases?: readonly string[];
  // ...plus customerBenefit, target, undo, eventName
}
The chat-side action definition. The model proposes by id and params; build() materializes a LuxoExecutableAction the client can dispatch.
interface

LuxoStreamComplete

type LuxoStreamComplete = (
  input: LuxoCompleteInput,
) => Promise<{
  stream: AsyncIterable<LuxoStreamChunk>;
  requestId: string;
  modelUsed?: string;
  providerUsed?: string;
}>;
Added in v0.3.0. Streaming completer signature. Drives the SSE response when the client sends Accept: text/event-stream.
interface

LuxoStreamChunk

type LuxoStreamChunk =
  | { type: "text-delta"; text: string }
  | { type: "reasoning-delta"; text: string }
  | { type: "stop"; reason?: string };
Subset of agent-core’s LLMStreamChunk that the chat route cares about. Tool-call streaming is filtered out by the route — Luxo actions arrive via the assembled !> directive in the final message, not as streaming tool input.
interface

LuxoPersistenceAdapter

interface LuxoPersistenceAdapter {
  loadConversation?: (ctx) => Promise<{ id: string; messages: LuxoChatMessage[] } | null>;
  saveMessage?: (ctx) => Promise<void>;
  endConversation?: (ctx) => Promise<void>;
  listConversations?: (ctx) => Promise<Array<{ id: string; title: string | null; updatedAt: string }>>;
  autoTitle?: (ctx) => Promise<string | null>;
}
Hook for persistence. Each method is independently optional — implement only what your product needs. Use createSupabaseLuxoPersistence for the reference Supabase shape (loadConversation, saveMessage, endConversation, listConversations); wire autoTitle from createLuxoAutoTitler when you want generated titles.
interface

LuxoQuotaAdapter

interface LuxoQuotaAdapter {
  check(actor: LuxoActor): Promise<LuxoQuotaState>;
}

interface LuxoQuotaState {
  allowed: boolean;
  kind?: "free-monthly" | "pro-daily" | string;
  limit?: number | null;
  remaining: number | null;
  resetAt?: string | null;
  tier?: "free" | "pro" | string;
  used?: number;
}
Per-actor quota check. allowed: false surfaces a 429 with the quota details. The client also receives the same shape via the quota prop on LuxoClient — when provided, the kit gates sends client-side (skipping the round-trip when the user is already over the limit) and renders the blocker UI from this state. The optional tier field lets products render tier-specific copy (added in v0.6.0).
interface

LuxoI18nDictionary

interface LuxoI18nDictionary {
  authRequired?: string;
  quotaExceeded?: string;
  invalidBody?: string;
  invalidOrigin?: string;
  invalidUrl?: string;
  needUserMessage?: string;
  providerNotConfigured?: string;
  providerUnreachable?: string;
  openingActionFallback?: string;
}
Translation dictionary for every user-facing string emitted by createLuxoChatRoute. Apps using next-intl / lingui / etc. populate this from their loaded catalog before passing the manifest. English defaults preserved when keys are absent.

Persistence and quota are optional

The route works without persistence (no message history) and without quota (no rate limits). Add an adapter when your product needs them — both run inside the same request lifecycle, so failures surface as part of the response.

Tool registry · declarative actions

Added in v0.8.0. Build a typed, surface-aware action registry with defineLuxoRegistry + the navigateTool / eventTool / guidanceTool builders. The registry is the source of truth for what actions Luxo can propose on each page — including risk, params, group, pinning, priority, and auto-generated directive examples emitted into the system prompt so the LLM knows the exact wire format.

import { defineLuxoRegistry, navigateTool, eventTool, guidanceTool, textParam, enumParam, integerParam, numberParam, booleanParam } from "@luxoai-dev/luxo-assistant";
defineLuxoRegistrynavigateTooleventToolguidanceTooltextParamenumParamintegerParamnumberParambooleanParamformatLuxoActionPromptLineLuxoToolRegistryLuxoActionDefinitionLuxoActionParamDefinition
Function

defineLuxoRegistry

since v0.8Beta
function defineLuxoRegistry(
  input: DefineLuxoRegistryInput,
): LuxoToolRegistry
Compose product identity, surfaces, and tools into a single registry. Returns a LuxoToolRegistry whose .manifest is drop-in compatible with createLuxoChatRoute and LuxoRoot. Duplicate action ids throw at registration so dev mistakes surface immediately. Surface-scoped tools auto-attach to their surface key; top-level actions remain global.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import {
  defineLuxoRegistry,
  navigateTool,
  eventTool,
  enumParam,
  integerParam,
} from "@luxoai-dev/luxo-assistant";

export const registry = defineLuxoRegistry({
  serviceId: "supplyagent",
  productName: "SupplyAgent",
  audience: "operators",
  domain: "supply",
  defaultSurface: {
    key: "automations",
    label: "Automations",
    shortLabel: "Auto",
    accentLabel: "Workspace",
    summary: "Run and supervise automations.",
    tooltip: "Ask Luxo about this run.",
    paths: ["/automations"],
    tools: [
      navigateTool({
        id: "automation.open",
        label: "Open automation",
        description: "Open a specific automation by id.",
        url: ({ id }) => `/automations/${id}`,
        params: { id: enumParam(["a1", "a2"], { description: "Automation id.", required: true }) },
        group: "Workspace",
        pinned: true,
        priority: 10,
      }),
      eventTool({
        id: "automation.run",
        label: "Run automation",
        description: "Trigger a dry-run preview for an automation.",
        eventName: "supplyagent:run",
        params: { rows: integerParam({ description: "Row count.", min: 1, max: 1000, example: 10 }) },
        risk: "confirm",
      }),
    ],
  },
});
interface

navigateTool · eventTool · guidanceTool

navigateTool(input): LuxoActionDefinition   // kind="navigate", risk default "safe"
eventTool(input): LuxoActionDefinition       // kind="event",    risk default "confirm"
guidanceTool(input): LuxoActionDefinition    // kind="none",     risk default "safe"
Each builder fills in kind, risk, and undo defaults so you only specify what's distinctive. eventTool defaults to risk: "confirm" — the kit gates execution behind a confirmation prompt (see actionBeaconEndpoint + requiresConfirmation).
interface

Param helpers

textParam(input)        // type="text"
enumParam(values, input)// type="enum"
integerParam(input)     // type="integer", validated /^-?\d+$/ + min/max
numberParam(input)      // type="number",  validated Number.isFinite + min/max
booleanParam(input)     // type="boolean", validated /^(true|false)$/i

// All accept: { description, required?, defaultValue?, example?, min?, max?, maxLength? }
Param definitions feed two systems: (1) directive validation in materializeLuxoAction — invalid LLM proposals drop silently, (2) auto-generated directive examples in the system prompt so the model emits the correct payload shape.
interface

LuxoActionDefinition (v0.8 fields)

interface LuxoActionDefinition {
  // Identity
  id: string;
  label: string;
  description: string;
  aliases?: readonly string[];

  // Behavior
  kind?: "navigate" | "event" | "none";
  url?: string | ((params) => string);
  eventName?: string;
  build?: (params) => LuxoExecutableAction | null;
  formatLabel?: (params) => string;                  // v0.8

  // Risk & confirmation
  risk: "safe" | "confirm" | "restricted";
  availability?: "public" | "authenticated" | "pro" | "admin";
  requiresConfirmation?: boolean;
  confirmation?: {                                   // v0.8
    title?: string;
    description?: string;
    confirmLabel?: string;
    cancelLabel?: string;
  };
  undo?: "not_needed" | "browser_back" | "explicit";

  // Surface scoping
  surface?: string | readonly string[];

  // Discovery & UI
  group?: string;                                    // v0.8 — drawer groups
  pinned?: boolean;                                  // v0.8 — sidebar rail
  priority?: number;                                 // v0.8 — surface ordering
  customerBenefit?: string;
  promptExample?: string;                            // v0.8 — overrides auto-generated

  // Params
  params?: Record<string, LuxoActionParamDefinition>;
  target?: "_self" | "_blank";
}

risk: "confirm" now gates execution

Before v0.8, risk: "confirm" was advisory metadata. In v0.8 the kit wraps execution with window.confirm by default. Audit your existing registries: downgrade purely cosmetic uses to risk: "safe", or accept the dialog. Custom modals via a onRequestConfirmation hook are planned for v0.9.

Client surface

Imported from @luxoai-dev/luxo-assistant. Drop LuxoRoot into a client layout, ship @luxoai-dev/luxo-assistant/styles.css, and the orb + drawer + voice mode are all wired.

import { LuxoRoot } from "@luxoai-dev/luxo-assistant";
LuxoRootLuxoClientLuxoDrawerLuxoOrbLuxoCaptionLuxoMessageFeedbackMessageContentuseLuxoChatuseVoiceModebuildLuxoSystemInstructionparseLuxoResponseDirectivesresolveLuxoSurfacesetLuxoSurfaceContextcreateLuxoAutoTitlerLuxoClientPropsLuxoExecutableActionLuxoErrorReporterLuxoErrorStageLuxoFeedbackProp
Component

LuxoRoot

Beta
function LuxoRoot(props: LuxoClientProps): JSX.Element
The drop-in client. Renders the orb, drawer, voice mode, captions, and starter-prompt UI. Mount once at the layout level; it manages its own visibility per route via hideOnPaths.
Parameters
ParamTypeRequiredDescription
props.manifestLuxoChatManifestRequiredSame manifest you pass to createLuxoChatRoute.
props.userLuxoActor | nullOptionalAuthenticated actor. Pass null for signed-out state.
props.apiPathstringOptionalWhere Luxo POSTs its requests. Defaults to '/api/luxo/chat'.
props.requireAuthbooleanOptionalShow the sign-in CTA when user is null. Defaults to true.
props.onSignIn() => voidOptionalCalled when the user clicks the sign-in CTA.
props.drawerSide'left' | 'right'OptionalSide the drawer slides from. Defaults to 'right'.
props.orbSide'left' | 'right'OptionalSide the orb floats. Defaults to drawerSide.
props.enableVoicebooleanOptionalVoice mode toggle. Defaults to true.
props.fullScreenHrefstringOptionalIf set, the drawer shows a 'full page' link to this URL.
props.hideOnPathsreadonly string[]OptionalPathnames where the orb is hidden (e.g. ['/login', '/signup']).
props.authLoadingbooleanOptionalRender a loading state instead of the orb until auth resolves.
props.streambooleanOptionalAdded in v0.3.0. When true the client sends Accept: text/event-stream and renders text-delta chunks in real time. Transparently falls back to the buffered path if the server replies with JSON.
props.feedbackboolean | { mode?: 'thumbs' | 'stars'; endpoint?: string }OptionalAdded in v0.4.0. true enables thumbs-up/down on every assistant message that has an llmRequestId; { mode: 'stars' } switches to a 5-star picker; { endpoint } overrides the default '/api/luxo/feedback' POST target; false (default) opts out.
props.quotaLuxoQuotaState | nullOptionalAdded in v0.6.0. External quota observation. When provided, the kit gates sends client-side (skipping the round-trip when over limit) and renders blocker UI from this state.
props.onMessageSent() => voidOptionalAdded in v0.6.0. Fires once per successful send. Wire to a quota tracker's increment() for optimistic UI.
props.onErrorLuxoErrorReporterOptionalAdded in v0.6.0. Receives (error, { stage, metadata }) for every caught error across the chat / stream / action / persistence pipeline. Default falls through to console.error. Useful for routing into your monitoring stack.
props.onExecuteAction(action: LuxoExecutableAction) => boolean | voidOptionalAdded in v0.6.0. Consumer override for action execution. Return true to skip the kit's default behavior (navigate / dispatch CustomEvent); return false / undefined to fall through. Enables custom action handlers (Zustand stores, confirmation flows, custom event buses).
props.actionBeaconEndpointstringOptionalAdded in v0.7.0. When set, every executed assistant action fires navigator.sendBeacon to this URL with { actionId, surface, conversationId }. Falls back to fetch({ keepalive: true }) on older browsers; silently no-ops in SSR. Pair with createLuxoActionBeaconRoute server-side.
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// app/(authenticated)/layout.tsx
"use client";
import { LuxoRoot } from "@luxoai-dev/luxo-assistant";
import "@luxoai-dev/luxo-assistant/styles.css";
import { manifest } from "@/lib/luxo/manifest";
import { useAuth } from "@/lib/auth";
import { useLuxoQuota } from "@/hooks/useLuxoQuota";

export default function AuthedLayout({ children }: { children: React.ReactNode }) {
  const { user, loading } = useAuth();
  const quota = useLuxoQuota(user, loading);

  return (
    <>
      {children}
      <LuxoRoot
        manifest={manifest}
        user={user}
        authLoading={loading}
        apiPath="/api/luxo/chat"
        hideOnPaths={["/login", "/signup", "/onboarding"]}
        // Opt-in features (all default to off):
        stream                                 // v0.3+ — SSE text-deltas
        feedback                               // v0.4+ — inline 👍/👎 per message
        quota={quota.snapshot}                 // v0.6+ — pre-flight quota gate + blocker UI
        actionBeaconEndpoint="/api/luxo/action" // v0.7+ — action-execution beacon
        onMessageSent={() => quota.increment()}
        onError={(error, ctx) =>
          console.error("[luxo-assistant]", error, {
            metadata: ctx.metadata,
            stage: ctx.stage,
          })
        }
      />
    </>
  );
}

Manifest in a shared module

Put the manifest in lib/luxo/manifest.ts and import it from both the server route and the client layout. That guarantees server + client agree on surfaces, actions, and starter prompts.
Hook

useLuxoChat

Beta
function useLuxoChat(options: {
  apiPath: string;
  manifest: LuxoChatManifest;
  surface: ResolvedLuxoSurface;
  surfaceContext?: Record<string, unknown>;
  user?: LuxoActor | null;
  authLoading?: boolean;
  requireAuth?: boolean;
  pathname?: string | null;
  stream?: boolean;                              // v0.3+
  quota?: LuxoQuotaState | null;                 // v0.6+
  onMessageSent?: () => void;                    // v0.6+
  onError?: LuxoErrorReporter;                   // v0.6+
  onExecuteAction?: (action) => boolean | void;  // v0.6+
}): {
  messages: LuxoChatMessage[];
  conversationId: string | null;                 // v0.7+
  draft: string;
  setDraft: (text: string) => void;
  sendMessage: (text?: string) => Promise<void>;
  clearConversation: () => void;
  isLoading: boolean;
  error: unknown;
  authRequired: boolean;
  quotaExceeded: boolean;
  quotaInfo: LuxoQuotaState | null;
  lastActions: LuxoExecutableAction[];
  lastFollowUps: string[];
}
Lower-level hook used by LuxoClient. Useful when you’re building a custom client surface (e.g. an in-app side panel or a full-screen chat page that doesn’t want the orb). All extension props on LuxoClient are also accepted here — the surface component is a thin wrapper around this hook.
Hook

useVoiceMode

Beta
function useVoiceMode(options: {
  enabled?: boolean;
  onTranscript?: (text: string) => void;
}): {
  active: boolean;
  start: () => Promise<void>;
  stop: () => void;
  /* ... */
}
Web Speech API voice input mode. Used by LuxoRoot when enableVoice is true. Browser support varies — falls back to a no-op when SpeechRecognition isn't available.
Function

buildLuxoSystemInstruction

Beta
function buildLuxoSystemInstruction(input: {
  actor: LuxoActor;
  manifest: LuxoChatManifest;
  messages: LuxoChatMessage[];
  pageTitle: string | null;
  pathname: string;
  surface: ResolvedLuxoSurface;
  surfaceContext?: Record<string, unknown>;
  system: string;
}): string
Build the canonical Luxo system prompt from a manifest + surface + context. Used internally by createLuxoChatRoute; exported so you can re-render the prompt for tests or alternate completers.
Function

parseLuxoResponseDirectives

Beta
function parseLuxoResponseDirectives(
  text: string,
  manifest: LuxoChatManifest,
  surface: ResolvedLuxoSurface,
): {
  text: string;
  actions: LuxoExecutableAction[];
  followUps: string[];
}
Strip !> action and ?> follow-up directives from a Luxo response. Materializes actions against the manifest’s definitions (validates id, applies build()). Used by the server route; export here in case you have a non-route consumer.
Function

resolveLuxoSurface

Beta
function resolveLuxoSurface(
  manifest: LuxoChatManifest,
  pathname: string,
): ResolvedLuxoSurface
Resolve the active surface for a pathname. Iterates surfaces[].paths / .match() and falls back to defaultSurface.
Component

LuxoMessageFeedback

since v0.4Beta
function LuxoMessageFeedback(props: LuxoMessageFeedbackProps): JSX.Element | null
Standalone inline-feedback component used internally by LuxoClient when the feedback prop is set. Renders a compact thumbs UI (hover-revealed via CSS) that expands a comment box on 👎. Use the standalone export only when you’re building a custom message renderer — for the standard chat surface, pass feedback to LuxoClient instead. Style hooks live under .luxo-message-feedback in @xdanielsb/luxo-assistant/styles.css.
Parameters
ParamTypeRequiredDescription
props.requestIdstringRequiredThe assistant message's llmRequestId. Used as the FeedbackEntry.requestId on the server.
props.endpointstringOptionalPOST target. Defaults to '/api/luxo/feedback'.
props.mode'thumbs' | 'stars'Optional'thumbs' (default) or a 5-star picker.
Function

setLuxoSurfaceContext

Beta
function setLuxoSurfaceContext(
  patch: Partial<LuxoSurfaceContext>,
): void
Write a context snapshot for the active surface. The chat hook reads the snapshot at send time and the server’s system prompt builder renders it as natural language. Use this when the assistant needs to know what the user is currently looking at (selected ticket, active circuit, lesson progress, etc.).
example.tsTypeScriptExample
1
2
3
4
5
6
7
8
9
useEffect(() => {
  setLuxoSurfaceContext({
    learner: {
      level: lessonXp.summary.level,
      totalXp: lessonXp.summary.totalXp,
      explored: lessonXp.summary.lessonsExplored,
    },
  });
}, [lessonXp.summary]);
interface

LuxoExecutableAction

type LuxoExecutableAction =
  | { id: string; type: "navigate"; url: string; label?: string }
  | { id: string; type: "event"; eventName: string; payload?: unknown; label?: string }
  | { id: string; type: string; [key: string]: unknown };
The materialized action a model proposed via a !> directive, after registry validation. Passed to your onExecuteAction override. Return true to skip the kit’s default behavior (navigate / dispatchEvent); return false/ undefined to fall through. Apps can extend the type with product-specific shapes (e.g. quantumagent’s circuit_edit / wiring_edit).
interface

LuxoErrorReporter

type LuxoErrorStage =
  | "send"
  | "stream"
  | "persistence"
  | "action"
  | "quota";

type LuxoErrorReporter = (
  error: unknown,
  context: { stage: LuxoErrorStage; metadata?: Record<string, unknown> },
) => void;
Receives every caught error across the chat pipeline. Stage tells you which subsystem failed; metadata is shape-free product context (e.g. the failing action id, the persistence method name). Default reporter falls through to console.error; consumers typically forward to their monitoring provider.
interface

LuxoFeedbackProp

type LuxoFeedbackProp =
  | boolean
  | {
      mode?: "thumbs" | "stars";
      endpoint?: string;
    };
Shape of the feedback prop on LuxoClient. Pass true for thumbs at the default endpoint, { mode: "stars" } for the 5-star picker, or an object with a custom endpoint when your feedback route lives somewhere else.
Module summary
SubpathUse it when
@luxoai-dev/luxo-assistantYou're rendering the assistant in a client component (LuxoRoot, hooks).
@luxoai-dev/luxo-assistant/serverYou're handling chat requests in a Next.js route handler.
@luxoai-dev/luxo-assistant/styles.cssAlways — once, in your client layout. Required for the orb / drawer styles.