Neocortex
IntegrationsWeb SDK

JavaScript API

Drive Neocortex characters from your own UI or game loop

The objects behind the components, for your own UI or game loop. Names mirror the Unity SDK (SmartAgent is NeocortexSmartAgent, and so on).

Entry points

Import fromContains
@neocortex-link/web/clientSmartAgent, GroupDirector, QuizAgent, AudioReceiver, StreamingAudioPlayer, UsageGate, EventLogger, Interactables, LocalSessionStore, MemorySessionStore, primeAudio, getAudioContext
@neocortex-link/webNeocortexClient, NeocortexError, isNeocortexError, EMOTIONS, toEmotion, VERSION and the API types
@neocortex-link/web/servercreateNeocortexHandler, toNodeHandler, MemoryRateLimiter, NeocortexClient, NeocortexError, isNeocortexError
@neocortex-link/web/uiThe element classes, defineNeocortexElements, setDefaultClient, and everything from @neocortex-link/web

Every class takes endpoint (default /api/neocortex) or a client, which wins.

Example: voice in, voice out

import { SmartAgent, AudioReceiver } from "@neocortex-link/web/client";

const agent = new SmartAgent({ endpoint: "/api/neocortex", characterId: "your-character-id" });

agent.on("lineStarted", ({ line }) => subtitle.show(line.text, line.emotion));
agent.on("emotionChanged", ({ emotion }) => avatar.setExpression(emotion));
agent.on("composing", (thinking) => spinner.toggle(thinking));
agent.registerAction("FOLLOW_PLAYER", () => npc.follow(player));

// Typed input, spoken reply
sendButton.onclick = () => {
  agent.primeAudio(); // browsers only allow audio after a click
  agent.textToAudio(input.value);
};

// Spoken input, spoken reply
const mic = new AudioReceiver({ maxWaitTime: 1.2 });
mic.on("recorded", (wav) => agent.audioToAudio(wav));

talkButton.onclick = async () => {
  agent.primeAudio();
  await mic.start(); // asks for microphone permission, then records when the player speaks
};

Speech streams line by line, prefetching the next line. A line that fails to synthesize is shown as text. Input during a turn is queued. Call primeAudio() from a user gesture so later speech can play.

Events

Every class except EventLogger and Interactables is an emitter:

const off = agent.on("lineStarted", (detail) => { /* ... */ }); // returns a function that removes the handler
agent.once("replyFinished", (reply) => { /* ... */ });
agent.off("lineStarted", handler);

A throwing handler is logged and doesn't stop the others.

SmartAgent

One character in conversation with the player.

const agent = new SmartAgent({ endpoint: "/api/neocortex", characterId: "your-character-id", voice: true });
await agent.send("Where is the blacksmith?");

Conversation options

SmartAgent and GroupDirector share these options:

OptionDefaultDescription
endpoint/api/neocortexYour proxy route.
clientA configured NeocortexClient. Takes precedence over endpoint.
voicefalseSpeak replies aloud. Can also be chosen per call.
paceLinestrueShow multi-line replies one line at a time.
actionTrigger"whenResponseReceived"When registered actions run: "whenResponseReceived", "whenSpeechStarts" or "afterReplySpoken".
sessionStoreLocalSessionStorePersists the session between page loads. Pass null to start fresh every time, or a MemorySessionStore to forget it when the page closes.
interactablesAn Interactables whose entities are sent with each turn.
eventLoggerAn EventLogger whose events are sent with each turn.
languageLanguage for speech and transcription, for example "en" or "tr".
playera new StreamingAudioPlayerThe player that plays speech.

LocalSessionStore uses the Unity SDK's keys (neocortex-session-id_<characterId>) and falls back to memory where localStorage is unavailable. An unknown session is cleared and the next turn starts fresh.

Methods

MethodDescription
textToText(message)Sends a typed message and shows the reply as text.
textToAudio(message)Sends a typed message and speaks the reply.
audioToText(audio)Transcribes a recording, sends it, and shows the reply as text.
audioToAudio(audio)Transcribes a recording, sends it, and speaks the reply.
send(message)Sends a message using the agent's voice setting.
prompt()Lets the character speak without the player saying anything, for example a greeting.
transcribe(audio)Transcribes a recording only. Resolves with the text, or null.
history(limit = 20, before?)Loads one page of this conversation's history, newest page first. Pass the page's nextCursor as before for the next. Resolves with null when there is no conversation yet.
registerAction(name, handler)Runs handler(action, message) whenever the character asks for the action name. Names are matched case-insensitively. Returns a function that removes the handler.
unregisterAction(name)Removes a handler.
clearSession()Forgets the conversation. The next turn starts a new session.
abort()Cancels the turn in flight and anything queued, and stops speech.
primeAudio()Unlocks audio output. Call it from a click or key press.

The send methods resolve with the character's message { characterId, name, lines, actions } once it has been played, or null if the turn failed. Failures are reported through the error event, not thrown.

Properties

characterId, sessionId, isBusy, isSpeaking, client, player, and the settable voice, paceLines, actionTrigger and language.

Events

EventPayload
busytrue while a turn is in flight or being played.
composingtrue while waiting on the server or between paced lines. Show a thinking indicator.
playerMessageWhat the player said, typed or transcribed.
transcriptionThe text of a transcribed recording.
responseThe whole reply { sessionId, messages }, as soon as it arrives.
speaker{ characterId, name }, when a character begins its part of the reply.
lineStarted{ characterId, name, line, index }. line is { text, emotion, spokenText?, speechToken? }.
emotionChanged{ characterId, emotion }
action{ characterId, action, message }, for every action, with or without a handler.
messageA character finished its part of the reply.
replyFinishedEvery character has finished.
historyA page of history loaded by history().
errorA NeocortexError.

Emotions are camelCase: neutral, happy, pleased, disappointed, upset, amazed, curious, confused, alarmed, fascinated, impressed, annoyed, angry, confident, reassured, concerned, scared. Anything else is read as neutral.

GroupDirector

Several characters in one conversation, where the AI picks who answers. See Group Chat for a walkthrough.

import { GroupDirector } from "@neocortex-link/web/client";

const group = new GroupDirector({ endpoint: "/api/neocortex", characterIds: ["aria", "bram"], voice: true });
group.on("speaker", ({ name }) => highlight(name));
await group.send("What happened at the mill?");

Takes the conversation options, plus characterIds and an optional maxTurns (how many characters may speak in one turn). The proxy allows up to 6 characters a turn by default.

MethodDescription
send(message)The player speaks to the group, and the AI decides who answers.
sendTo(characterId, message)The player addresses one character directly.
continue()The characters carry on among themselves without the player.
sendAudio(audio)Transcribes a recording and sends it to the group.
addCharacter(characterId)Adds a character. The cast is part of the session, so this starts a new one.
removeCharacter(characterId)Removes a character and starts a new session. The last character cannot be removed.

The send methods resolve with the whole reply { sessionId, messages }, or null. members lists the current cast. Everything else (events, registerAction, history, abort and so on) works as on SmartAgent.

QuizAgent

A quiz host that asks questions, judges answers and keeps score (Team plan). See Quiz for a walkthrough.

import { QuizAgent } from "@neocortex-link/web/client";

const quiz = new QuizAgent({ endpoint: "/api/neocortex", characterId: "host-id", questionSetId: "set-id", voice: true });
quiz.on("hostLine", ({ line }) => subtitle.show(line.text));
quiz.on("questionChanged", (question) => renderOptions(question?.options ?? []));
quiz.on("finished", () => showResults(quiz.leaderboard));

startButton.onclick = () => {
  quiz.primeAudio();
  quiz.begin();
};
OptionDefaultDescription
characterIdrequiredThe host.
questionSetIdrequiredThe question set to play.
endpoint / clientAs above.
participants[{ id, displayName, type? }], where type is "human" or "ai". The proxy passes on the first 10.
questionCountHow many questions to ask.
selectionMode"sequential" or "random".
voicefalseSpeak the host's lines aloud.
queueInputWhileBusytrueHold input that arrives mid-turn and send it afterwards. When false, it is dropped with an inputDropped event.
languageLanguage for speech.
playera new StreamingAudioPlayerThe player that plays speech.
playerIdIgnored behind the proxy, which always uses the signed-in user's id.
MethodDescription
begin(participants?)Starts a new quiz, replacing any current one.
say(text, { typed })The player answers, asks for a hint, says "repeat" and so on, in their own words. Set typed: true when the text was typed rather than spoken.
sayAs(participantId, text, { typed })The same, for one participant.
choose(optionId) / chooseAs(participantId, optionId)Picks one of the question's options.
pass() / passAs(participantId)Skips the question.
resync()Re-reads the quiz state from the server, for example after a reconnect.
abort()Cancels the turn in flight and stops speech.
primeAudio()Unlocks audio output.

The state of the run is kept on the agent: sessionId, question, progress ({ asked, total }), expecting ("answer", "confirmation" or "nothing"), leaderboard, lastResult, lastIntent, attempt, attemptsLeft, hintGiven, pending, answered, isFinished and isBusy.

EventPayload
busytrue while a turn is running.
hostLine{ line, index }, when a host line appears.
emotionChangedThe host's new emotion.
hostFinishedThe turn, once the host has finished its lines.
questionChangedThe new question, after the host has read it out, or null when the quiz ends.
upcoming{ questionId, keyword? } for the next question.
expectingChangedWhat the host is waiting for.
resultThe verdicts for the last answer.
leaderboardChangedThe standings.
turnEvery turn, as the server sent it.
finishedThe closing turn.
inputDropped{ reason }: "busy", "finished" or "not_started".
creditsExhaustedThe team ran out of credits.
errorA NeocortexError.

AudioReceiver

Records the player's voice with automatic speech detection or push-to-talk, on an AudioWorklet. In detection mode it listens continuously, starts recording when loudness crosses the threshold, and finishes after maxWaitTime seconds of silence. Recordings come out as 16 kHz mono WAV, ready for audioToAudio.

import { AudioReceiver } from "@neocortex-link/web/client";

const mic = new AudioReceiver({ pushToTalk: true });
mic.on("recorded", (wav) => agent.audioToText(wav));

talkButton.onpointerdown = () => mic.press();
talkButton.onpointerup = () => mic.release();
OptionDefaultDescription
pushToTalkfalseRecord only between press() and release(), instead of detecting speech.
amplitudeThreshold0.1Loudness from 0 to 1 that counts as speech.
maxWaitTime1.2Seconds of silence that end a recording.
maxRecordingSeconds30Longest recording.
keepListeningfalseKeep listening for the next phrase after one is recorded.
deviceIdthe last selected deviceA specific input device.
MemberDescription
start()Opens the microphone. In detection mode, recording begins when the player speaks.
stop()Closes the microphone, including one still waiting on permission. A recording in progress is discarded.
press() / release()Push-to-talk: start recording, then stop and emit it. press() opens the microphone if needed.
selectDevice(deviceId)Switches microphone and remembers the choice for next time.
AudioReceiver.devices()Lists microphones. Labels are only filled in once permission has been granted.
AudioReceiver.isSupported()Whether this browser can record.
state, amplitude, isListening, isRecording, silenceProgressCurrent state. silenceProgress goes from 0 to 1 as silence approaches maxWaitTime, for a countdown ring.
EventPayload
recordedThe recording as a WAV Blob. Recordings shorter than 0.2 seconds are dropped.
speechStartedRecording began.
state"idle", "listening" or "recording".
amplitudeMicrophone loudness from 0 to 1, about 20 times a second.
permissionGrantedThe player allowed the microphone.
permissionDeniedThe player blocked the microphone.
errorAnything else that went wrong.

StreamingAudioPlayer

Plays a character's voice while it is still being synthesized, through Web Audio. Every agent has one as its player, so you can read the speech level for lip sync:

function animate() {
  avatar.mouthOpen = agent.player.getLevel(); // 0 to 1
  requestAnimationFrame(animate);
}
animate();
MemberDescription
new StreamingAudioPlayer({ prebufferSeconds })prebufferSeconds defaults to 0.15, so a slow first block does not stutter.
play(chunks, { onStart, signal })Plays streamed PCM chunks, such as stream.chunks from client.streamAudio(). Resolves once playback ends or is stopped.
playBlob(blob, { onStart, signal })Plays a whole clip, such as the result of client.generateAudio().
stop()Stops playback.
getLevel()How loud the speech is right now, from 0 to 1.
volumeOutput volume from 0 to 1.
isPlayingWhether anything is playing.
prime()Unlocks audio output. Call it from a user gesture.

Pass one player to several agents with the player option to share one volume. A player plays one thing at a time, so a new line stops whatever it was playing.

UsageGate

Checks credits before offering a conversation, so you can fall back to scripted lines. Fails open.

import { UsageGate } from "@neocortex-link/web/client";

const gate = new UsageGate({ endpoint: "/api/neocortex", characterId: "your-character-id" });
await gate.refresh();

if (gate.canUse()) await agent.send(text);
else showScriptedLine();
MemberDescription
new UsageGate({ endpoint, client, characterId, minRefreshSeconds })minRefreshSeconds defaults to 30. Calls inside that window reuse the last answer.
refresh(force = false)Checks usage and resolves with it, or with the last answer if the check failed.
canUse()false only when the last check said credits, or this player's or character's allowance, ran out.
startAutoRefresh(seconds) / stopAutoRefresh()Refreshes on an interval, at least every 5 seconds.
usageThe last answer: { status, creditsRemaining, player?, character? }, where status is "ok", "low" or "empty".

Events: updated, lowCredits, creditsEmpty, playerOverLimit, characterOverLimit, each with the usage, and error when the check itself failed.

Behind the proxy, usage is checked for the signed-in user. Caps are set on the API Keys page.

EventLogger

Collects game events for characters to know about, such as "the player opened the vault". Each entry is cut to 64 characters, and a turn sends at most 20, highest priority first, then newest.

import { EventLogger, SmartAgent } from "@neocortex-link/web/client";

const events = new EventLogger();
const agent = new SmartAgent({ endpoint: "/api/neocortex", characterId: "guard", eventLogger: events });

events.push("The player picked the vault lock", 2);
MethodDescription
push(content, priority = 1, date = new Date())Adds an event. priority is 0 (low), 1 (medium) or 2 (high).
getLogs(limit = 20)The events a turn would send.
clear()Removes every event.

Interactables

The things in a scene a character can perceive and act on. Actions come back with the targetId of the entity they apply to. See Interactables for a walkthrough.

import { Interactables, SmartAgent } from "@neocortex-link/web/client";

const scene = new Interactables()
  .set({ id: "door-1", type: "OBJECT", name: "Cellar door", properties: [{ name: "locked", value: "true" }] })
  .set({ id: "lamp-1", type: "OBJECT", name: "Oil lamp" })
  .focus("door-1");

const agent = new SmartAgent({ endpoint: "/api/neocortex", characterId: "guard", interactables: scene });
agent.registerAction("UNLOCK", (action) => unlock(action.targetId));
MethodDescription
set(item)Adds or replaces an entity: { id, type, name, characterId?, isSubject?, position?, properties? }, where type is "CHARACTER" or "OBJECT".
update(id, patch)Changes some fields of an entity.
remove(id) / get(id) / clear()Manage entities.
focus(id)Marks one entity as what the player is focused on (isSubject), and clears the rest. Pass null to clear all.
toMetadata()The entities as sent with a turn.

set, update, remove and focus return the same object, so calls can be chained.

The proxy drops browser metadata and events unless allowClientContext: true.

NeocortexClient

A typed wrapper over every v3 endpoint. Create one to add headers, change timeouts, or call endpoints directly.

import { NeocortexClient } from "@neocortex-link/web";
import { SmartAgent } from "@neocortex-link/web/client";

const client = new NeocortexClient({
  baseUrl: "/api/neocortex",
  headers: () => ({ authorization: `Bearer ${getToken()}` }),
});
const agent = new SmartAgent({ client, characterId: "your-character-id" });
OptionDefaultDescription
baseUrl/api/neocortex, or https://api.neocortex.link/v3 with an apiKeyWhere requests go.
apiKeyThe secret team key. Only for servers, or browser prototyping with dangerouslyAllowBrowser.
dangerouslyAllowBrowserfalseAllows an apiKey in browser code. See Prototyping without a server.
headersExtra headers for every request, as an object or a function called per request.
credentials"same-origin"Cookie policy for requests. Use "include" for a proxy on another origin that relies on cookies.
fetchglobalThis.fetchA custom fetch.
timeouts{ chat: 30000, audio: 20000, lookup: 15000 }Milliseconds to wait for a response to start.
MethodEndpoint
chat(request)POST /chat
chatHistory({ sessionId, limit?, before? })POST /chat/session
generateAudio({ characterId, message, emotion?, format?, language?, speechToken? })POST /audio/generate, resolves with an audio Blob
streamAudio({ characterId, message, emotion?, language?, speechToken? })POST /audio/stream, resolves with { chunks, cancel() }
transcribe({ characterId, audio, language?, prompt? })POST /audio/transcribe, resolves with { response }
quizTurn(request)POST /quiz
quizSession(sessionId)GET /quiz/session
usage({ playerId?, characterId? })GET /usage
account()GET /account
characters()GET /characters
questionSets()GET /question-sets

Every method takes an optional second argument { signal, timeoutMs }. quizTurn retries the server's "still busy" refusals the same way the Unity SDK does.

Server-to-server

On a backend that calls Neocortex directly, give the client your key. baseUrl then defaults to the Neocortex API:

import { NeocortexClient } from "@neocortex-link/web/server";

const neocortex = new NeocortexClient({ apiKey: process.env.NEOCORTEX_API_KEY });
const { sessionId, messages } = await neocortex.chat({
  characterIds: ["your-character-id"],
  message: "Hi",
  playerId: "user-1",
});

Errors

Every failure is a NeocortexError. Agents emit it on error; NeocortexClient throws it.

MemberDescription
statusThe HTTP status, or 0 when no response arrived.
codeThe machine-readable reason, when there is one. Branch on this.
messageA readable description.
retryAfterSeconds to wait before retrying, when the server said so.
turnFor quiz_finished, the closing turn the quiz already played.
isOutOfCreditstrue when the team has run out of credits, so retrying will not help.
isAbortedtrue when the request was cancelled.

Codes raised in the browser are timeout, network_error, aborted, client_error and browser_api_key. Codes from the proxy are listed in Proxy Options.

Use isNeocortexError(error) rather than instanceof, so errors from a second copy of the package (CJS next to ESM) match too.

import { isNeocortexError } from "@neocortex-link/web";

try {
  await client.chat({ characterIds: ["your-character-id"], message: "Hi" });
} catch (error) {
  if (isNeocortexError(error) && error.code === "rate_limited") {
    await wait((error.retryAfter ?? 5) * 1000);
  }
}

On this page