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.
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
| Field | Type | Description |
|---|---|---|
text | string | The full accumulated response text so far |
partial | string | The text content of just this chunk |
isFinal | boolean | true when the response is complete |
messageId | string | Unique identifier for this response |
chunkIndex | number | Sequential index of this chunk (starts at 0) |
isInterjection | boolean | true 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
botResponseandbotVoiceevents assendText().isInterjectionisfalse. - Pass
textOnly: trueto 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).
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
- Voice (WebSocket) -- Add voice input and output
- Voice (LiveKit) -- Low-latency voice via WebRTC
- Action System -- Handle action tags in bot responses