OptionalonCalled when the agent moves between idle, loading, talking and running a tool.
Use it to drive a typing indicator or to disable input while the agent is busy. The full set of states is reported for Expressive (V4) agents only. Talks (V2) and Clips (V3) agents report just Talking and Idle, and only on fluent streams, plus a final Idle when the connection closes.
It is the authoritative "is the agent speaking" signal wherever it reports at all — an Expressive (V4) session or a fluent stream — because it comes from the agent rather than from the video track, so it also covers thinking and tool calls, which produce no video. On a legacy stream it reports nothing, so use onVideoStateChange there; that is the branch every consumer otherwise writes for itself, keyed on getStreamType().
The AgentActivityState the agent has moved to.
OptionalonCalled every time the connection to the agent's stream changes state.
Triggered by connect(), reconnect() and disconnect(). 'connected' is the point at which chat() and speak() can be called.
The state just reached: one of new, connecting, connected, completed,
disconnecting, disconnected, closed or fail. See ConnectionState.
Optionalreason: string
Why that state was reached. On disconnected it is a
StreamEndReason value when the server ended the stream on purpose, which is how you
tell a deliberate end from a dropped connection. Talks (V2) and Clips (V3) agents report
only ok, unknown_error, network_issue and inactivity; message_limit, time_limit
and ended_by_agent come from Expressive (V4) agents. A close reason the SDK does not
recognize is forwarded as-is, so compare reason against the enum rather than parsing it;
any other value is an opaque transport diagnostic.
reconnect() still works after a deliberate end; it starts a
new stream rather than resuming the old one.
import { ConnectionState, type AgentManagerCallbacks } from '@d-id/client-sdk';
const callbacks: AgentManagerCallbacks = {
onConnectionStateChange(state, reason) {
console.log('onConnectionStateChange(): ', state, reason);
if (state === ConnectionState.Connected) {
console.log("I'm ready to go!");
}
},
};
OptionalonCalled when the quality of the user's internet connection changes.
The state is derived from jitter-buffer delay and freeze count on the video stream for Talks (V2) and Clips (V3) agents, and from LiveKit's reported connection quality for Expressive (V4) agents, so it reflects what the session is actually getting rather than the browser's online flag.
OptionalonCalled when the SDK fails, so the application can surface the problem.
Receives failures from the Agents API requests, the stream and the web socket — an HttpError when a request comes back non-2xx, a NetworkError when it never reaches the server, a WsError when the web socket itself fails, and also ChatModeDowngraded and StreamError. Two errors do not arrive here: ValidationError and ChatCreationFailed are thrown to whoever called the method (chat(), speak() and the rating methods), so they surface as a rejected promise rather than through this callback.
The error that occurred. Narrow it with isDIDError and branch on kind; toJson() is what to log.
OptionalerrorData: ErrorContext
Where the failure happened, as an ErrorContext: the endpoint and method for a failed request, the session or stream id for a failure on the stream. Every field is optional and nothing else is passed — in particular not the request body, which carries the end user's own message.
OptionalonCalled when the agent becomes interruptible, or stops being interruptible.
Expressive (V4) agents only, and about whether interrupting is allowed right now: it goes
false while a blocking client tool call is outstanding, because the agent is suspended
waiting for it, and back to true when the call finishes. That is a different question from
isInterruptAvailable(), which says whether
the session supports interrupting at all. Use this one to enable or disable an interrupt
button. Talks (V2) and Clips (V3) agents never report a change.
true while there is something to interrupt, false while there is not.
OptionalonCalled when the chat mode changes.
Fires after changeMode(), and when the server answers with a different mode than the one requested (for example ChatMode.Maintenance).
import { ChatMode, type AgentManagerCallbacks } from '@d-id/client-sdk';
const callbacks: AgentManagerCallbacks = {
onModeChange(mode) {
// `chat()` rejects in Maintenance too, so the composer goes with the banner below.
setComposerEnabled(
mode !== ChatMode.Off && mode !== ChatMode.DirectPlayback && mode !== ChatMode.Maintenance
);
if (mode === ChatMode.Maintenance) {
showBanner('The agent is temporarily unavailable.');
}
},
};
OptionalonCalled when a chat is created for this session.
A chat is created while connect() runs, and lazily on the first chat() when none exists yet. Store the id if you want to correlate the conversation with your own records.
On Talks (V2) and Clips (V3) agents the id comes from the Agents API. On Expressive (V4)
agents the SDK derives it from the session id as cht_<sessionId>, which is the same id the
Agents API stores the conversation under — so it correlates with D-ID's own records either
way.
Id of the chat that was just created.
OptionalonCalled with the whole chat transcript every time a message is added or updated.
Triggered by chat(), by speak() for
text scripts, and as the agent's answer streams in. It also fires once while the manager is
created, with whatever initialMessages were
given, and again on every connect() after the first — both
with type answer. The array is a fresh copy on each call, oldest message first; every
Message carries an id, a role of user or assistant (the agent), its content
and a createdAt timestamp.
import type { AgentManagerCallbacks } from '@d-id/client-sdk';
const callbacks: AgentManagerCallbacks = {
onNewMessage(messages, type) {
// `partial` fires repeatedly as the answer streams in; `answer` is the final one.
if (type === 'answer') {
console.log(messages[messages.length - 1].content);
}
},
};
OptionalonCalled whenever the set of tool calls running in the session changes.
Expressive (V4) agents only. On disconnect it fires with an empty array if any call was still running, so a spinner driven by this callback always clears; with nothing outstanding there is nothing to clear and nothing is emitted. Each entry is a RunningToolCall.
Every tool call running right now, empty when none is.
OptionalonCalled with the media stream carrying the agent's video and audio.
This is the one callback a video session cannot work without: assign the value to the
srcObject of your <video> element, and keep a reference to it, because
onVideoStateChange has to put it back
after the idle video has been shown. Triggered by
connect(), reconnect() and
disconnect().
Optional only for the chat modes that never stream video — ChatMode.TextOnly, ChatMode.Playground and ChatMode.Maintenance — so a text-only application does not have to supply a stub. createAgentManager rejects with a ValidationError when it is missing in any other mode — ChatMode.Off and ChatMode.DirectPlayback included, since those create no chat but still stream video. connect() checks it again against the mode in effect then, so a manager created in a text-only mode and later moved into a video mode with changeMode() rejects rather than opening a stream with nothing to render it into.
The live media stream to render.
import type { AgentManagerCallbacks } from '@d-id/client-sdk';
// Kept so `onVideoStateChange` can put it back after the idle video.
let srcObject: MediaStream | null = null;
const callbacks: AgentManagerCallbacks = {
onSrcObjectReady(value) {
videoElement.srcObject = value;
srcObject = value;
},
};
OptionalonCalled once per session when the stream has been created on the server.
Fires before the connection reaches 'connected'. The ids are useful when correlating a session with D-ID support or with your own logs.
The new stream's streamId, sessionId and agentId.
OptionalonCalled when the agent starts, finishes or fails a tool call.
Expressive (V4) agents only. The handler takes two arguments and returns nothing: event,
one of ToolCallEvent.Started, ToolCallEvent.Done or
ToolCallEvent.Error; and data, the payload that event carries —
ToolCallStartedPayload, ToolCallDonePayload or
ToolCallErrorPayload respectively. The signature that does the narrowing is on
ToolEventCallback, which also carries a handler that branches on all three events.
OptionalonCalled when the streamed video starts and stops, so the video element can switch source.
Triggered by chat() and speak(). On
STOP point the element at the agent's idle video (Agent.idle_video); on START put
the stream handed to onSrcObjectReady back
on it.
On a legacy stream this is the only "is the agent speaking" signal — see onAgentActivityStateChange for which stream types report which.
import { StreamingState, type AgentManagerCallbacks } from '@d-id/client-sdk';
let srcObject: MediaStream | null = null;
const callbacks: AgentManagerCallbacks = {
onSrcObjectReady(value) {
srcObject = value;
videoElement.srcObject = value;
},
onVideoStateChange(state) {
if (state === StreamingState.Stop) {
videoElement.srcObject = null;
videoElement.src = agentManager.agent.idle_video ?? '';
} else {
videoElement.src = '';
videoElement.srcObject = srcObject;
}
},
};
Handlers the SDK calls as the connection, the video stream and the chat change state.
Pass the object as AgentManagerOptions.callbacks. Every handler is optional in the type, but onSrcObjectReady is required in practice for any chat mode that streams video — without it there is nothing to render the agent into, and createAgentManager rejects. Every handler is called from the SDK's own event handling, so keep the work inside short.
The handlers fall into four groups: the connection (onConnectionStateChange, onConnectivityStateChange, onError), the video stream (onSrcObjectReady, onStreamCreated, onVideoStateChange, onAgentActivityStateChange, onInterruptibleChange), the chat (onNewChat, onNewMessage, onModeChange) and client tools (onToolEvent, onRunningToolCallsChange). A handler that lives outside the object can be typed with an indexed access such as
AgentManagerCallbacks['onNewMessage'].The whole object is captured once, by createAgentManager, and the manager works from its own copy — assigning a handler to the object you passed afterwards has no effect, and neither does replacing options.callbacks. Give each handler a stable identity that reads the current state rather than closing over it: in React, keep the state in a ref and read
ref.currentinside the handler.