Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,5 @@ vite.config.ts.timestamp-*

# macOS Finder metadata
.DS_Store

/apps/web-demo/
6 changes: 3 additions & 3 deletions AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pnpm --filter <name> typecheck
Exports from `agora-agent-client-toolkit`:

- `AgoraVoiceAI` — main singleton class (async `init()`)
- `AgoraVoiceAIConfig`, `RTMConfig` — config interfaces
- `AgoraVoiceAIConfig` — config interface
- `AgoraVoiceAIEvents` — event name constants
- `CovSubRenderController` — transcript rendering controller
- `ChunkedMessageAssembler` — stream message assembly
Expand All @@ -54,7 +54,7 @@ Exports from `agora-agent-client-toolkit-react`:
## Constraints

- **Do not modify `CovSubRenderController`** without explicit task scope. It is battle-tested rendering logic; bugs here are silent and hard to reproduce without real agent traffic.
- **RTM is optional** — never assume `rtmEngine` is present. Use `rtmConfig?.rtmEngine`.
- **RTM is optional** — never assume `rtmEngine` is present.
- **`AgoraVoiceAI.init()` is async** — always `await`.
- **pnpm only** — no npm or yarn commands.
- **`jszip` and `@agora-js/report` are optional deps** — guard all usages.
Expand All @@ -65,7 +65,7 @@ Exports from `agora-agent-client-toolkit-react`:
// Core config
interface AgoraVoiceAIConfig {
rtcEngine: IAgoraRTCClient; // required
rtmConfig?: { rtmEngine: RTMClient }; // optional
rtmEngine?: RTMClient; // optional
renderMode?: TranscriptHelperMode; // TEXT | WORD | AUTO
enableLog?: boolean;
enableAgoraMetrics?: boolean;
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ Migration notes for each release should link to the matching section in [MIGRATI
`AGENT_STATE_CHANGED` remains supported and emitted; React `agentState` APIs
remain the aggregate compatibility surface.

## [v2.9.1] - 2026-07-20

### Fixed

- Restored the `ConversationalAIAPI` export and standardized RTM config on top-level `rtmEngine`.
- Fixed WORD-to-TEXT fallback to preserve rendered history and discard pending WORD data.

### Changed

- Bumped the core and React npm packages to `2.9.1`.

### Upgrade notes

- Migration guide: see [MIGRATION.md#290---291](./MIGRATION.md#290---291).

## [v2.9.0] - 2026-07-10

Stable release for the 2.9.0 ConvoAI API line.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The rendering controller is the most complex and highest-risk module in the code

- `AgoraVoiceAI` is a **singleton** — `init()` creates it, `getInstance()` retrieves it, `destroy()` clears it.
- `AgoraVoiceAI.init()` is **async** — always `await` it.
- RTM is **optional** — `rtmConfig?: { rtmEngine }`. Three methods throw if called without it: `sendText`, `sendImage`, `interrupt`.
- RTM is **optional** — use the top-level `rtmEngine` field. RTM-backed methods throw if it is omitted.
- `AgoraVoiceAIConfig` is defined in `src/core/config.ts`. `src/core/conversational-ai.ts` re-exports it — don't define it there.

## Package names
Expand Down
15 changes: 15 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@ Use this file for all version-to-version upgrade steps.

## Version index

- [2.9.0 -> 2.9.1](#290---291)
- [1.2.x -> 2.9.0](#12x---290)
- [1.1.x -> 1.2.0](#11x---120)

---

## 2.9.0 -> 2.9.1

This release restores the `ConversationalAIAPI` export and its legacy enum/type names. All
initialization paths now use the top-level `rtmEngine` field. Replace
`rtmConfig: { rtmEngine }` with `rtmEngine` when upgrading.

`enableRenderModeFallback` defaults to `true`. In WORD mode, messages without word timing data
switch rendering to TEXT while preserving text already emitted. Set it to `false` to keep the
previous WORD-only behavior.

Keep `agora-agent-client-toolkit-react` and `agora-agent-client-toolkit` on the same version.

---

## 1.2.x -> 2.9.0

### TL;DR
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pnpm add agora-agent-client-toolkit-react agora-agent-client-toolkit agora-rtc-r

## Migration

Upgrading from an earlier release? See the [migration guide](./MIGRATION.md), including the steps for `1.2.x -> 2.9.0`.
Upgrading from an earlier release? See the [migration guide](./MIGRATION.md), including the steps for `2.9.0 -> 2.9.1`.

### Optional dependencies

Expand Down Expand Up @@ -58,7 +58,7 @@ rtcClient.on('user-published', async (user, mediaType) => {
// --- Add Conversational AI features ---
const ai = await AgoraVoiceAI.init({
rtcEngine: rtcClient,
rtmConfig: { rtmEngine: rtmClient },
rtmEngine: rtmClient,
});

ai.on(AgoraVoiceAIEvents.TRANSCRIPT_UPDATED, (transcript) => {
Expand Down Expand Up @@ -113,7 +113,7 @@ function App() {
const config = useMemo(
() => ({
channel: 'my-channel',
rtmConfig: { rtmEngine: rtmClient },
rtmEngine: rtmClient,
}),
[]
);
Expand Down Expand Up @@ -155,20 +155,20 @@ function VoiceSession() {

| Package | Version | Description |
| ---------------------------------------------------------------------------- | ------- | ---------------------------------- |
| [`agora-agent-client-toolkit`](./packages/conversational-ai/README.md) | 2.9.0 | Core SDK — vanilla JS / TypeScript |
| [`agora-agent-client-toolkit-react`](./packages/react/README.md) | 2.9.0 | React hooks |
| [`agora-agent-client-toolkit`](./packages/conversational-ai/README.md) | 2.9.1 | Core SDK — vanilla JS / TypeScript |
| [`agora-agent-client-toolkit-react`](./packages/react/README.md) | 2.9.1 | React hooks |

Full API reference, configuration options, and events are in each package's README.

## RTC-only mode (no RTM)

RTM is optional. Transcripts and agent state work without it — just omit `rtmConfig`:
RTM is optional. Transcripts and agent state work without it — just omit `rtmEngine`:

```typescript
const ai = await AgoraVoiceAI.init({ rtcEngine: rtcClient });
```

RTM-backed methods throw without `rtmConfig`: `sendText`, `sendImage`, `interrupt`, `manualSOS`, and `manualEOS`.
RTM-backed methods throw without `rtmEngine`: `sendText`, `sendImage`, `interrupt`, `manualSOS`, and `manualEOS`.

## Repository layout

Expand Down
4 changes: 2 additions & 2 deletions apps/demo/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ console.log('✓ RTM Client created');
*
* Options:
* - rtcEngine: Your RTC client instance (required)
* - rtmConfig: Optional RTM config object — omit to run RTC-only
* - rtmEngine: Optional RTM client — omit to run RTC-only
* (sendText, sendImage, and interrupt will throw if RTM is absent)
* - renderMode: Transcript rendering mode (TEXT, WORD, CHUNK, AUTO, UNKNOWN)
* - enableLog: Enable debug logging
Expand All @@ -70,7 +70,7 @@ console.log('✓ RTM Client created');
async function initVoiceAI() {
const voiceAI = await AgoraVoiceAI.init({
rtcEngine: rtcClient,
rtmConfig: { rtmEngine: rtmClient },
rtmEngine: rtmClient,
renderMode: TranscriptHelperMode.TEXT,
enableLog: true,
});
Expand Down
2 changes: 1 addition & 1 deletion apps/playground/src/components/SessionProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ interface Props {
export function SessionProvider({ credentials, rtcClient, rtmClient, onDisconnect }: Props) {
const config = useMemo(
() => ({
rtmConfig: rtmClient ? { rtmEngine: rtmClient } : undefined,
rtmEngine: rtmClient ?? undefined,
renderMode: credentials.renderMode,
enableLog: credentials.enableLog,
channel: credentials.channelName,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agora-agent-client-toolkit",
"version": "2.9.0",
"version": "2.9.1",
"private": true,
"description": "Client-side SDK for adding Agora Conversational AI Engine features.",
"scripts": {
Expand Down
37 changes: 19 additions & 18 deletions packages/conversational-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ await rtmClient.login({ token: 'YOUR_RTM_TOKEN' });
// 2. Initialize the AI singleton
const ai = await AgoraVoiceAI.init({
rtcEngine: rtcClient,
rtmConfig: { rtmEngine: rtmClient },
rtmEngine: rtmClient,
renderMode: TranscriptHelperMode.WORD,
});

Expand Down Expand Up @@ -95,9 +95,10 @@ The following parameters must be set when starting the AI agent via the Agora RE
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `rtcEngine` | `RTCEngine` | Yes | Structural RTC client contract (`on/off` for `'audio-pts'` and `'stream-message'`) |
| `rtmConfig` | `{ rtmEngine: RTMEngine }` | No | Structural RTM client contract (`publish`, `addEventListener`, `removeEventListener`) |
| `rtmEngine` | `RTMEngine` | No | Structural RTM client contract (`publish`, `addEventListener`, `removeEventListener`) |
| `renderMode` | `TranscriptHelperMode` | No | `TEXT`, `WORD`, `CHUNK`, or `AUTO`. If omitted, defaults to `AUTO` — mode is detected from the first agent message. |
| `enableLog` | `boolean` | No | Enable debug logging (default: `false`) |
| `enableRenderModeFallback` | `boolean` | No | Fall back from `WORD` to `TEXT` when word timing data is missing (default: `true`) |
| `enableAgoraMetrics` | `boolean` | No | Load `@agora-js/report` for usage metrics (default: `false`) |

`RTCEngine` and `RTMEngine` are toolkit-exported structural interfaces. A normal `agora-rtc-sdk-ng` client and `agora-rtm` client satisfy them directly, and no `as unknown as` casts are required.
Expand All @@ -119,13 +120,13 @@ AgoraVoiceAI.getInstance(): AgoraVoiceAI // throws NotInitializedError if not y
ai.subscribeMessage(channel: string): void
ai.unsubscribe(): void
ai.chat(agentUserId: string, message: ChatMessageText | ChatMessageImage): Promise<void>
ai.sendText(agentUserId: string, message: ChatMessageText): Promise<void> // requires rtmConfig
ai.sendImage(agentUserId: string, message: ChatMessageImage): Promise<void> // requires rtmConfig
ai.interrupt(agentUserId: string): Promise<void> // requires rtmConfig
ai.manualSOS(agentUserId: string, requestId?: string): Promise<string> // requires rtmConfig
ai.manualEOS(agentUserId: string, requestId?: string): Promise<string> // requires rtmConfig
ai.sendText(agentUserId: string, message: ChatMessageText): Promise<void> // requires rtmEngine
ai.sendImage(agentUserId: string, message: ChatMessageImage): Promise<void> // requires rtmEngine
ai.interrupt(agentUserId: string): Promise<void> // requires rtmEngine
ai.manualSOS(agentUserId: string, requestId?: string): Promise<string> // requires rtmEngine
ai.manualEOS(agentUserId: string, requestId?: string): Promise<string> // requires rtmEngine
ai.destroy(): void
ai.getCfg(): { rtcEngine, renderMode, channel, enableLog }
ai.getCfg(): { rtcEngine, rtmEngine, renderMode, channel, enableLog, enableRenderModeFallback }
ai.on(event, handler): void
ai.off(event, handler): void
```
Expand Down Expand Up @@ -170,7 +171,7 @@ Word-level timing is at `metadata.words` — not at the top level.

---

#### Agent activity events _(requires `rtmConfig`)_
#### Agent activity events _(requires `rtmEngine`)_

Use the independent activity events for new integrations. More than one flag
may be active at the same time.
Expand All @@ -191,7 +192,7 @@ ai.on(AgoraVoiceAIEvents.AGENT_SPEAKING_CHANGED, (agentUserId, active) => {

---

#### `AGENT_STATE_CHANGED` _(deprecated, requires `rtmConfig`)_
#### `AGENT_STATE_CHANGED` _(deprecated, requires `rtmEngine`)_

This event is deprecated but remains supported and continues to be emitted.
Existing integrations do not need to migrate. Use the independent activity
Expand Down Expand Up @@ -255,7 +256,7 @@ ai.on(AgoraVoiceAIEvents.AGENT_ERROR, (agentUserId, error) => {

---

#### `MESSAGE_RECEIPT_UPDATED` _(requires `rtmConfig`)_
#### `MESSAGE_RECEIPT_UPDATED` _(requires `rtmEngine`)_

Fires when a delivery or read receipt is received for a sent message.

Expand All @@ -269,7 +270,7 @@ ai.on(AgoraVoiceAIEvents.MESSAGE_RECEIPT_UPDATED, (agentUserId, receipt) => {

---

#### `MESSAGE_ERROR` _(requires `rtmConfig`)_
#### `MESSAGE_ERROR` _(requires `rtmEngine`)_

Fires when a chat message fails to deliver.

Expand All @@ -283,7 +284,7 @@ ai.on(AgoraVoiceAIEvents.MESSAGE_ERROR, (agentUserId, error) => {

---

#### `MESSAGE_SAL_STATUS` _(requires `rtmConfig`)_
#### `MESSAGE_SAL_STATUS` _(requires `rtmEngine`)_

Fires when the Speech Activity Level (SAL) registration status changes.

Expand All @@ -297,7 +298,7 @@ ai.on(AgoraVoiceAIEvents.MESSAGE_SAL_STATUS, (agentUserId, salStatus) => {

---

#### `USER_MANUAL_SOS` / `USER_MANUAL_EOS` / `AGENT_MANUAL_EOS` _(requires `rtmConfig`)_
#### `USER_MANUAL_SOS` / `USER_MANUAL_EOS` / `AGENT_MANUAL_EOS` _(requires `rtmEngine`)_

Manual turn control is a two-step flow. `manualSOS()` and `manualEOS()` publish an RTM marker and resolve with the `requestId` used in the payload. That only means RTM publish succeeded; server validation arrives later through events.

Expand Down Expand Up @@ -368,7 +369,7 @@ Advanced: implement `IMetricsReporter` or use the exported `ConsoleMetricsReport
| Error Class | When Thrown | Recovery |
|------------|------------|----------|
| `NotInitializedError` | `getInstance()` or `getCfg()` called before `init()` | Call `await AgoraVoiceAI.init(config)` first |
| `RTMRequiredError` | `sendText()`, `sendImage()`, `interrupt()`, `manualSOS()`, or `manualEOS()` called without RTM | Pass `rtmConfig: { rtmEngine }` in `init()` config |
| `RTMRequiredError` | `sendText()`, `sendImage()`, `interrupt()`, `manualSOS()`, or `manualEOS()` called without RTM | Pass `rtmEngine` in `init()` config |
| `ConversationalAIError` | `chat()` called with unsupported message type | Check `message.messageType` is TEXT or IMAGE |

All error classes extend `ConversationalAIError`, which extends `Error`. Use `instanceof` to catch specific error types.
Expand Down Expand Up @@ -400,7 +401,7 @@ try {
});
} catch (e) {
if (e instanceof RTMRequiredError) {
console.error('RTM not configured — pass rtmConfig to init()');
console.error('RTM not configured — pass rtmEngine to init()');
}
}

Expand Down Expand Up @@ -455,11 +456,11 @@ See the [Agora Conversational AI documentation](https://docs.agora.io/en/convers

**Cause:** Standalone hooks (`useTranscript`, `useAgentState`, etc.) need access to the `AgoraVoiceAI` instance. Without a `ConversationalAIProvider`, they fall back to a single `getInstance()` attempt which may miss the instance if `init()` hasn't completed yet.

**Fix:** Wrap your component tree in `ConversationalAIProvider` so standalone hooks connect through React context. Pass `rtmConfig` when the child hooks consume RTM-backed state or controls:
**Fix:** Wrap your component tree in `ConversationalAIProvider` so standalone hooks connect through React context. Pass `rtmEngine` when the child hooks consume RTM-backed state or controls:

```tsx
<ConversationalAIProvider
config={{ channel: 'my-channel', rtmConfig: { rtmEngine: rtmClient } }}
config={{ channel: 'my-channel', rtmEngine: rtmClient }}
>
<TranscriptPanel /> {/* useTranscript() connects via context */}
<StatusBar /> {/* useAgentState() connects via context */}
Expand Down
Loading
Loading