Skip to main content

Text Chat

Start a text-based conversation with a Stellar agent. No microphone or audio setup is needed — the SDK handles the WebSocket connection and streams agent responses as text.

import { createStellarClient } from "@stellar-ai/agent-sdk";

const client = createStellarClient();

const chat = await client.startChatConversation({
auth: {
strategy: "publicToken",
token: "<your-public-access-token>",
},
agentId: "<your-agent-id>",
visitorId: getOrCreateVisitorId(), // unique ID for this visitor
});

chat.sendMessage("Hello, I need help with my order.");

The SDK automatically creates a conversation on the platform before connecting the WebSocket. Each client supports one active chat conversation at a time.

Options

OptionTypeRequiredDescription
authAuthConfigYesAuthentication configuration
agentIdstringYesThe agent to connect to
visitorIdstringYesUnique identifier for this visitor
conversationIdstringNoResume a previous conversation
entryPoint"AI" | "HUMAN"NoRoute to AI (default) or human queue
variablesVariablesNoInitial context for the agent
metadataRecord<string, string>NoVisitor context (page URL, etc.)

Sending and receiving messages

Send messages with chat.sendMessage(). Agent responses arrive as streaming events:

chat
.on("messageDelta", ({ messageId, delta }) => {
// Streaming text from the agent, arriving in chunks
process.stdout.write(delta);
})
.on("messageDone", ({ messageId, content }) => {
console.log("\nAgent:", content);
});

chat.sendMessage("What's the status of order ORD-12345?");

messageDelta events deliver text incrementally as the agent generates it. messageDone fires once with the complete message when the agent finishes. Both events include a messageId so you can associate deltas with their final message.

Resuming a chat

Pass a conversationId to reconnect to a previous session. The SDK checks if the conversation is still active — if it is, it reconnects and delivers the message history. If the conversation has ended, it creates a new one instead.

// First session — save the conversation ID
chat.on("started", ({ conversationId }) => {
localStorage.setItem("chatId", conversationId);
});

// Later — resume the conversation
const chat = await client.startChatConversation({
auth: { strategy: "publicToken", token: "<your-token>" },
agentId: "<your-agent-id>",
visitorId: getVisitorId(),
conversationId: localStorage.getItem("chatId"),
});

chat.on("started", ({ conversationId, resumed, messages }) => {
if (resumed) {
// Render previous messages
messages.forEach((msg) => renderMessage(msg.role, msg.content));
}
// Save the (possibly new) conversation ID
localStorage.setItem("chatId", conversationId);
});

The started event includes:

  • conversationId — the active conversation ID (may differ from what you passed if the old one expired)
  • resumedtrue if reconnected to the existing conversation, false if a new one was created
  • messages — previous message history (only present when resumed is true)

Each message in the history has the shape:

interface ChatMessage {
id: string;
role: "user" | "assistant" | "human" | "system";
content: string;
}

Events

EventPayloadDescription
stateChanged{ state }Connection state changed (connecting, connected, disconnected, error)
error{ error, code }An error occurred
started{ clientId, conversationId, resumed, messages? }Chat session ready
messageDelta{ messageId, delta }Streaming text chunk from the agent
messageDone{ messageId, content }Complete message from the agent
handover{}Agent initiated handover to a human
agentJoined{}A human agent joined the chat
queued{}Conversation placed in a queue
supervisorRequest{ question }Agent asked a human supervisor a question
supervisorResolved{}Supervisor question resolved
actionExecuted{ actionName, functionCallItemId, requestBody?, responseBody?, httpStatusCode?, httpStatusText?, error? }Agent executed a tool or action
guardrailTriggered{ name, executor, description, message, triggerReason }A guardrail was triggered
topicSet{ topic }The conversation topic was detected or set
stateTransition{ fromState?, fromStateName?, toState, toStateName, reason?, success, errorMessage? }The agent moved between dialogue states
compactionStarted{}The server started compacting a long conversation's context
compactionCompleted{}The server completed context compaction
ended{}Conversation ended by the server

Human handover events

The handover, agentJoined, and queued events support human-in-the-loop workflows. When the agent determines a human should take over, it emits handover. If the conversation is placed in a queue while waiting, you'll receive queued. Once a human agent picks up, agentJoined fires.

chat
.on("handover", () => {
showStatus("Connecting you to a human agent...");
})
.on("queued", () => {
showStatus("You're in the queue. We'll be with you shortly.");
})
.on("agentJoined", () => {
showStatus("A human agent has joined the conversation.");
});

Supervisor events

The supervisorRequest and supervisorResolved events handle scenarios where the AI agent consults a human supervisor mid-conversation without fully handing over. The agent pauses to ask a question, and resumes after receiving an answer.

chat
.on("supervisorRequest", ({ question }) => {
showStatus("The agent is consulting a supervisor...");
})
.on("supervisorResolved", () => {
showStatus("The agent is back.");
});

Action executed events

The actionExecuted event is the main way to handle Studio Client action executions in your app. When the agent chooses a client_action, the SDK emits this event with the Studio action name and the arguments the agent supplied. Your app can listen for it and map it to local behavior like navigation, opening native UI, or updating client-side state.

The same event also fires for other action types. In those cases, requestBody, responseBody, and the optional HTTP fields let you inspect what happened after the action ran. In chat, the payload also includes functionCallItemId, which you can use to correlate the event with the underlying function call.

chat.on("actionExecuted", ({ actionName, requestBody, error }) => {
const args = requestBody ? JSON.parse(requestBody) : {};

if (actionName === "navigate_to_screen") {
navigateToScreen(args.screen_name);
}

if (error) {
showStatus("The agent tried an action, but it failed.");
}
});

State, topic, and guardrail events

These events surface what the agent is doing under the hood, so you can build live observability into your chat experience.

  • topicSet fires when the agent detects or sets the conversation topic. Use it to label or route the conversation.
  • stateTransition fires when the agent moves between dialogue states. The payload includes success, and errorMessage when a transition fails.
  • guardrailTriggered fires when a guardrail activates, with the guardrail's name, description, message, and triggerReason.
chat
.on("topicSet", ({ topic }) => {
showTopic(topic);
})
.on("stateTransition", ({ toStateName, success }) => {
if (success) showStatus(`Now in: ${toStateName}`);
})
.on("guardrailTriggered", ({ name, message }) => {
console.warn(`Guardrail "${name}" triggered: ${message}`);
});

Server-ended conversations

The ended event fires when the server terminates the conversation (for example, due to inactivity or an agent-initiated close). Clean up your UI when you receive this event.

chat.on("ended", () => {
showStatus("This conversation has ended.");
disableChatInput();
});

Methods

sendMessage

Send a text message to the agent:

chat.sendMessage("What's the status of order ORD-12345?");

end

End the chat session and close the connection:

await chat.end();

state

The current connection state: connecting, connected, disconnected, or error.

on, off, and once

  • on(event, handler) — subscribe to an event. Returns the instance for chaining.
  • off(event, handler) — unsubscribe a handler.
  • once(event, handler) — subscribe for a single occurrence, then auto-unsubscribe.

Next steps