Skip to main content

Text Chat

Send text messages to your AI character and handle streaming responses.

Sending Text

Use sendText() to send a message after connecting:

client.sendText('What is the weather like today?');

The method fires a text event over the WebSocket. The server processes the message through the AI pipeline and streams the response back via botResponse events.

info

sendText() throws an EstuaryError with code NOT_CONNECTED if the client is not connected. Always call connect() first.

Receiving Responses

Bot responses arrive as a stream of chunks. Each chunk is a BotResponse object:

client.on('botResponse', (response) => {
if (response.isFinal) {
// Full response is ready
console.log('Complete response:', response.text);
} else {
// Partial chunk -- `partial` contains just this chunk's text
process.stdout.write(response.partial);
}
});

BotResponse Fields

FieldTypeDescription
textstringThe full accumulated response text so far
partialstringThe text content of just this chunk
isFinalbooleantrue when the response is complete
messageIdstringUnique identifier for this response
chunkIndexnumberSequential index of this chunk (starts at 0)
isInterjectionbooleantrue if this is a proactive message, not a reply to user input

Streaming Pattern

A typical response arrives as multiple events:

botResponse { chunkIndex: 0, partial: "The weather", isFinal: false, text: "The weather" }
botResponse { chunkIndex: 1, partial: " today is", isFinal: false, text: "The weather today is" }
botResponse { chunkIndex: 2, partial: " sunny and", isFinal: false, text: "The weather today is sunny and" }
botResponse { chunkIndex: 3, partial: " warm.", isFinal: true, text: "The weather today is sunny and warm." }

The text field accumulates across chunks, so on isFinal: true it contains the full response.

Text-Only Mode

sendText() defaults to textOnly: true -- the character replies with text only and no TTS audio. To also receive a voice response, pass false as the second argument:

// Text only (default) -- fastest, no TTS
client.sendText('Give me a summary of our conversation.');

// Text + voice -- character speaks the reply
client.sendText('Tell me a story about the forest.', false);

The text-only default keeps latency and bandwidth low for UI-driven interactions. Opt into voice when you actually want the character to speak.

Scripting Lines with sayLine

Use sayLine() when you want the character to say a specific prewritten line verbatim, without going through the LLM. This is useful for greetings, system prompts, narration, or any moment where you want exact wording.

// Scripted line -- character says this exactly, with TTS by default
client.sayLine('Welcome back, traveler. What brings you here today?');

// Scripted line, text only (no audio)
client.sayLine('System: connection restored.', true);

Behavior:

  • Any in-progress response is interrupted before the line plays.
  • The line is fed directly to TTS -- the LLM is not invoked.
  • The line is saved to chat history as a normal assistant message, so memory and downstream LLM turns will see it as something the character naturally said.
  • The response uses the same botResponse and botVoice events as sendText(). isInterjection is false.
  • Pass textOnly: true to suppress audio.
// Listen for the scripted reply
client.on('botResponse', (response) => {
if (response.isFinal) {
console.log('Character said:', response.text);
}
});

client.sayLine('I have been waiting for you.');

Scripting a Sequence of Lines

sayLine() sends a single line. Because the server interrupts any in-progress response when it receives a new scripted line, calling sayLine() several times in a row makes each call cut off the previous one — only the last line is actually spoken.

To play several lines, use playScript(). It paces them for you: it sends one line, waits for it to finish, then sends the next.

const script = client.playScript(
[
'Welcome to my shop, traveler!',
'I have wares, if you have coin.',
'Come back anytime.',
],
{ textOnly: false, lineGapMs: 250 },
);

client.on('scriptLineStarted', ({ index, text }) => console.log(`saying ${index}: ${text}`));
client.on('scriptComplete', ({ reason }) => console.log(`script ended: ${reason}`));

await script.done; // { reason: 'finished' }

The returned controller lets you steer playback:

script.pause(); // hold after the current line finishes
script.resume(); // continue
script.next(); // skip ahead to the next line now
script.stop(); // halt immediately and interrupt the current line

Lines may be plain strings (which use the script's default textOnly) or per-line overrides, e.g. { text: '(a silent stage direction)', textOnly: true }. The scriptComplete reason is one of 'finished', 'stopped', 'disconnected', or 'interrupted'. sayLines(lines, opts) is a convenience alias of playScript(lines, opts).

note

Starting a new script with playScript() stops any script that is still running, since both would otherwise compete for the same scripted-line channel.

Interrupting a Response

You can interrupt an in-progress response with interrupt(). This tells the server to stop generating and clears any queued audio playback:

// Interrupt the current response
client.interrupt();

// Optionally specify which message to interrupt
client.interrupt(response.messageId);

Listen for the server's confirmation:

client.on('interrupt', (data) => {
console.log('Response interrupted:', data.messageId);
});

Interjections

Sometimes the character sends a message without being prompted -- for example, a greeting when you first connect, or a follow-up question. These are marked with isInterjection: true:

client.on('botResponse', (response) => {
if (response.isInterjection && response.isFinal) {
console.log('Character said (unprompted):', response.text);
}
});

Example: Chat Loop

Here is a complete example of a text chat loop using Node.js readline:

import { EstuaryClient } from '@estuary-ai/sdk';
import * as readline from 'readline';

const client = new EstuaryClient({
serverUrl: 'https://api.estuary-ai.com',
apiKey: 'est_your_api_key',
characterId: 'your-character-uuid',
playerId: 'user-123',
});

client.on('botResponse', (response) => {
if (response.isFinal) {
console.log(`\nBot: ${response.text}\n`);
rl.prompt();
}
});

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'You: ',
});

async function main() {
await client.connect();
console.log('Connected! Type a message and press Enter.\n');
rl.prompt();

rl.on('line', (line) => {
const text = line.trim();
if (text) {
client.sendText(text);
} else {
rl.prompt();
}
});

rl.on('close', async () => {
await client.disconnect();
process.exit(0);
});
}

main().catch(console.error);

Next Steps