SDKs
React
Hooks for React and React Native over the client SDK — a conversation in a component, and the person's sessions, facts and relationships.
@lumavox/react is hooks over the client SDK, for React and React Native alike: a conversation that opens with a component and closes with it, and reads of what the REST routes hold. React 18 or later; no other dependency.
npm install @lumavox/react @lumavox/clientPut a client in a provider, and talk to an agent from any component below it:
import { createClient } from "@lumavox/client";import { AgentProvider, useAgent } from "@lumavox/react";
const agents = createClient({ baseUrl: "https://api.lumavox.ai", getToken: async () => (await fetch("/api/agent-key", { method: "POST" })).text(),});
export function App() { return ( <AgentProvider client={agents}> <Chat agentId="agt_..." /> </AgentProvider> );}
function Chat({ agentId }: { agentId: string }) { const agent = useAgent({ agentId }); return ( <> <p>{agent.turn?.text}</p> <button disabled={agent.state === "connecting"} onClick={() => agent.send("Hello!")}> Say hello </button> </> );}Make the client once, outside any component. Every hook also takes client directly, for an app with no provider.
useAgent
Section titled “useAgent”useAgent({ agentId, resume, title }, options); // the person's last conversation with it, or a new one; resume and title optionaluseAgent({ sessionId }, options); // one you openeduseAgent(null, options); // nobody yet: nothing is openedIt opens the session on mount, connects, and closes when the component goes; a new target opens a new conversation. The options are the client’s handlers — onText, onDone and the rest — kept in a ref, so a new function on every render never redials; and speech, thoughts, tools, client, audio, and onAudioInterrupted and onAudioResumed for an interruption.
| It returns | |
|---|---|
state, ready, turn, caption, relation, queued |
the conversation’s snapshot, re-rendered on every change |
session |
the session it opened, with its last messages, for a transcript to start from |
error |
what went wrong last — { kind, message }, its kind session, frame, closed or audio — cleared by the next turn |
speaking, recording |
the player is playing; the recorder is open |
interrupted |
the device has the audio — a call, Siri, another app: playback is paused |
send, startAudio, sendAudio, endAudio, cancel, mute, reconnect, close |
the conversation’s own |
startRecording, stopRecording |
recording through audio |
newSession() |
a new conversation with the same agent |
conversation |
the client’s Conversation itself |
A send made before the conversation exists waits for it. A session that answers after the component is gone is never connected.
Your app’s tools are tools, as the client’s: named when the conversation connects, and each call runs the latest render’s handler.
const agent = useAgent({ agentId }, { tools: { open_page: ({ page }) => { navigate(String(page)); return { opened: page }; }, },});audio is an AgentAudio — a player and a recorder in one object, yours to make and to dispose. With one, every reply is played as it arrives, and playback stops on cancel, mute, a new message or a spoken turn, for the rest of that reply’s frames too. startRecording({ rate, transcribeOnly }) opens the recorder first and the recording at the rate it was granted, so a refused microphone never opens one — and takes a turn’s own speech and thoughts, as send does; stopRecording({ transcribeOnly }) takes the turn, or answers just the words, and stopRecording({ discard: true }) throws the recording away. A dictation (transcribeOnly) leaves the reply playing.
interface AgentAudio { play(pcm: ArrayBuffer, frame: { sample_rate: number; channels: number; chunk: number; turn: string }): void; stop(): void; onDrained(fn: () => void): () => void; startCapture(onPcm: (pcm: ArrayBuffer) => void, options?: { rate?: number }): Promise<{ rate: number }>; stopCapture(): Promise<void>; dispose(): void; onInterruption?(fn: (interrupted: boolean) => void): () => void; handsFree?(): Promise<{ ok: true } | { ok: false; reason: string }>; // for listen() duck?(on: boolean): void; // the reply quieter while the person may be talking over it}An interruption, when the adapter tells one — a phone call, Siri, another app, the app leaving the screen — throws away a recording in progress as stopRecording({ discard: true }) would, calls onAudioInterrupted, and sets interrupted until the adapter says the audio is back (onAudioResumed). The adapter pauses its own playback meanwhile.
In a browser, the client’s createWebAudio() is one. Make it once, and call its prime() in the click that starts the conversation — browsers start sound only from a gesture:
import { createWebAudio } from "@lumavox/client/web-audio";import { useAgent } from "@lumavox/react";import { useMemo, useState } from "react";
function Talk({ agentId }: { agentId: string }) { const audio = useMemo(() => createWebAudio(), []); const [started, setStarted] = useState(false); const agent = useAgent(started ? { agentId } : null, { audio }); if (!started) { return ( <button onClick={() => { void audio.prime(); setStarted(true); }} > Start </button> ); } return ( <button onPointerDown={() => agent.startRecording()} onPointerUp={() => agent.stopRecording()}> {agent.recording ? "Listening…" : "Hold to talk"} </button> );}It records at 24 kHz whatever startRecording asks. On React Native, @lumavox/react-native is one install with these hooks and createAudio(), on iOS, Android and the web. Without audio or an onAudio handler, no speech is asked for — nothing would play it.
To draw against the voice — a face lip-synced, subtitles — subscribe to the audio you gave the hook, not to the hook: createWebAudio() and createAudio() tell every frame as it is queued, with when it starts on the player’s clock, and what is heard now (the clock). The hook plays through them as before.
Hands-free
Section titled “Hands-free”agent.listen() leaves the recorder open and turns every utterance into a turn, as the client’s listen does: 700 ms of silence ends one, and talking over the agent stops its voice once its first words are heard — for the rest of that reply. listening says it is on, hearing that someone is talking; stopListening() closes the recorder. Push-to-talk waits while it listens.
import { createWebAudio } from "@lumavox/client/web-audio";import { useAgent } from "@lumavox/react";import { useMemo } from "react";
function HandsFree({ agentId }: { agentId: string }) { const audio = useMemo(() => createWebAudio(), []); const agent = useAgent({ agentId }, { audio }); return ( <button onClick={() => { void audio.prime(); void (agent.listening ? agent.stopListening() : agent.listen()); }} > {agent.hearing ? "Hearing you…" : agent.listening ? "Listening — tap to stop" : "Talk hands-free"} </button> );}listen() answers whether it listens. Where the agent would hear itself — a browser that cannot cancel echo, an iPhone, or an audio adapter with no handsFree — it does not, and error says why, in words to show; push-to-talk works there as ever. listen({ assumeNoEcho: true }) when you know better: headphones on, say.
listen({ halfDuplex: true }) runs everywhere: the recorder stays open, but nothing is heard while the agent speaks, so it never hears itself — and talking never stops it (half-duplex). Offer it where listen() refused:
async function talk(agent: ReturnType<typeof useAgent>) { if (!(await agent.listen())) await agent.listen({ halfDuplex: true });}Reading
Section titled “Reading”Each returns { data, error, loading, refresh }, with error a PlatformError. There is no shared cache: two components reading the same thing read it twice. On TanStack Query or SWR, call client.sessions, client.facts and client.relations in your own queries instead.
| Hook | |
|---|---|
useSessions({ limit, cursor }) |
a page of the person’s conversations, most recent first, with nextCursor — pass it as cursor for the next page; null on the last |
useSession(id) |
one conversation and its latest messages, with messages (every one read so far, oldest first), hasOlder and loadOlder(), which reads back by the transcript’s cursor |
useFacts() |
what is remembered about the person — an empty list when nothing is — with forget(id) and forgetAll() |
useRelation(agentId) |
how one agent feels about them: live from a useAgent talking to it under the same provider, read once otherwise |