Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6ec98e3
Add AI transport as a product within docs
GregHolmes Dec 2, 2025
ad8d01b
chore: Add AI Transport examples filter
matt423 Dec 15, 2025
11abcc7
chore: Add AI Transport product tile to the homepage
matt423 Dec 15, 2025
60c8146
ait/token-streaming: add message per token page
mschristensen Dec 9, 2025
ab6b7b4
ait/message-per-token: add intro
mschristensen Dec 9, 2025
65824dd
ait/message-per-token: add token publishing
mschristensen Dec 9, 2025
4016fff
ait/message-per-token: token streaming patterns
mschristensen Dec 9, 2025
1602247
ait/message-per-token: client hydration patterns
mschristensen Dec 10, 2025
367564d
ai-transport: add message per response doc
zknill Dec 11, 2025
58bf1b8
fix nav and typos
zknill Dec 11, 2025
42708b4
ai-transport/token-streaming: unify nav
mschristensen Dec 16, 2025
45fb720
ai-transport/token-streaming: refine intro
mschristensen Dec 16, 2025
f984333
ai-transport: refine Publishing section
mschristensen Dec 16, 2025
13e7f81
ai-transport: refine Subscribing section
mschristensen Dec 16, 2025
7664c8f
ai-transport: refine rewind section
mschristensen Dec 16, 2025
78833f1
ai-transport/token-streaming: refine history
mschristensen Dec 16, 2025
68ff77d
ai-transport/token-streaming: in-progress rewind
mschristensen Dec 16, 2025
232bf5d
ai-transport/token-streaming: in progress history
mschristensen Dec 16, 2025
ffef65e
ai-transport/token-streaming: remove metadata
mschristensen Dec 16, 2025
af944aa
ai-transport/token-streaming: add resume callout
mschristensen Dec 16, 2025
d6ab5eb
ai-transport/token-streaming: headers
mschristensen Dec 16, 2025
24ee5f7
ait: add sessions & identity docs
mschristensen Dec 10, 2025
c5a7d5b
chore: update message annotations terminology to include appends
matt423 Jan 5, 2026
7751bc3
ai-transport: misc message per response fixes
mschristensen Jan 5, 2026
8b4dd12
ait/guides: openai message per token
lawrence-forooghian Dec 10, 2025
fba8316
ait/guides: openai message per token fixes
mschristensen Jan 7, 2026
6141267
ait/features: fix messages per response anchor tag
mschristensen Jan 7, 2026
2df3e58
ait/guides: add open ai message per response guide
mschristensen Jan 7, 2026
b365cc9
ait/features: chain of thought
owenpearson Jan 5, 2026
a7697c4
ait/features: token streaming overview
GregHolmes Jan 12, 2026
0aa80ab
docs: add human-in-the-loop page for AI Transport
GregHolmes Jan 6, 2026
a5f9fe5
docs: add user input page for AI Transport
GregHolmes Jan 5, 2026
c0a06eb
feat: add Anthropic SDK message-per-token guide
matt423 Jan 8, 2026
a67749a
chore: update message annotations terminology to include appends
matt423 Jan 5, 2026
a20cd6a
Add citations feature documentation for AI Transport
mittulmadaan Jan 6, 2026
0e1fda4
AIT-133 - Fixed review comments
mittulmadaan Jan 13, 2026
6a7ae92
AIT-133 - fix lint issue + resolve conflicts
mittulmadaan Jan 13, 2026
693fb46
AIT-133 - nit fix
mittulmadaan Jan 13, 2026
14dea5f
Fixed review comments.
mittulmadaan Jan 13, 2026
7caf4ca
Update src/pages/docs/ai-transport/features/advanced/citations.mdx
mittulmadaan Jan 14, 2026
889ca2c
feat: Add AI Transport message per token example for Javascript
matt423 Dec 22, 2025
0ac0ff8
feat: Add AI Transport message per token example for React
matt423 Dec 22, 2025
86410f4
feat: Add guide for Anthropic SDK message-per-response streaming
matt423 Jan 14, 2026
d6fb047
chore: Add async/await for processEvent in OpenAI guide
matt423 Jan 14, 2026
ed3ddf8
ait/features: add tool call page
mschristensen Jan 13, 2026
426203f
ait/features: misc. fixes to citations docs
mschristensen Jan 14, 2026
d347b2f
AIT-238 - handle publish failures
JoaoDiasAbly Jan 16, 2026
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
60 changes: 60 additions & 0 deletions examples/ai-transport-message-per-token/javascript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# AI Transport message per token streaming

Enable realtime streaming of AI/LLM responses by publishing tokens as they arrive from Large Language Model services.

AI Transport token streaming allows applications to provide immediate, responsive AI interactions by streaming tokens in realtime rather than waiting for complete responses. This pattern is essential for creating engaging AI-powered experiences where users can see responses being generated as they happen.

The streaming approach significantly improves perceived performance and user engagement. Instead of waiting 5-10 seconds for a complete AI response, users see tokens appearing progressively, creating a more natural conversation flow similar to watching someone type in realtime.

Token streaming is implemented using [Ably AI Transport](/docs/ai-transport). AI Transport provides purpose-built APIs for realtime AI applications, offering reliable message delivery, automatic ordering, and seamless reconnection handling to ensure no tokens are lost during network interruptions.

## Resources

Use the following methods to implement AI Transport token streaming:

- [`client.channels.get()`](/docs/channels#create): creates a new or retrieves an existing channel for AI Transport token streaming.
- [`channel.subscribe()`](/docs/channels#subscribe): subscribes to token messages from AI services by registering a listener for realtime streaming.
- [`channel.publish()`](/docs/channels#publish): publishes individual tokens as they arrive from the LLM service with response tracking headers.
- [`channel.history()`](/docs/channels/history) with [`untilAttach`](/docs/channels/options#attach): enables seamless message recovery during reconnections, ensuring no tokens are lost.

Find out more about [AI Transport](/docs/ai-transport) and [message history](/docs/channels/history).

## Getting started

1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found:

```sh
git clone git@github.com:ably/docs.git
```

2. Change directory:

```sh
cd examples/
```

3. Rename the environment file:

```sh
mv .env.example .env.local
```

4. In `.env.local` update the value of `VITE_ABLY_KEY` to be your Ably API key.

5. Install dependencies:

```sh
yarn install
```

6. Run the server:

```sh
yarn run ai-transport-message-per-token-javascript
```

7. Try it out by opening [http://localhost:5173/](http://localhost:5173/) with your browser and selecting a prompt to see realtime AI token streaming.

## Open in CodeSandbox

In CodeSandbox, rename the `.env.example` file to `.env.local` and update the value of your `VITE_ABLY_KEY` variable to use your Ably API key.
46 changes: 46 additions & 0 deletions examples/ai-transport-message-per-token/javascript/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="src/styles.css" />
<title>AI Transport Token Streaming - JavaScript</title>
</head>

<body class="bg-gray-100">
<div class="max-w-6xl mx-auto p-5">
<!-- Response section with always visible status -->
<div class="mb-4">
<div class="flex-1">
<div class="text-sm text-gray-600 mt-4 mb-2 flex justify-between">
<span id="prompt-display"></span>
<div class="flex items-center gap-2">
<span class="text-xs bg-gray-200 px-2 py-1 rounded flex items-center gap-1">
<span id="processing-status">ready</span>
</span>
<!-- Disconnect/Reconnect button -->
<button id="connection-toggle" class="text-xs bg-blue-500 text-white px-2 py-1 rounded hover:bg-blue-600">
Disconnect
</button>
</div>
</div>
<div class="p-4 border border-gray-300 rounded-lg bg-gray-50 h-48 overflow-y-auto text-base leading-relaxed">
<span id="response-text">Select a prompt below to get started</span>
<span id="cursor" class="text-blue-600"></span>
</div>
</div>
</div>

<!-- Prompt selection -->
<div class="mb-4">
<div class="flex flex-wrap gap-2" id="prompt-buttons">
<button id="prompt-button" class="px-3 py-2 text-sm border rounded-md transition-colors">
What is Ably AI Transport?
</button>
</div>
</div>
</div>

<script type="module" src="src/script.ts"></script>
</body>
</html>
10 changes: 10 additions & 0 deletions examples/ai-transport-message-per-token/javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "ai-transport-message-per-token-javascript",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
63 changes: 63 additions & 0 deletions examples/ai-transport-message-per-token/javascript/src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Agent Service
// This consumes LLM streams and publishes events to Ably

import * as Ably from 'ably';
import { MockLLM } from './llm';

export class Agent {
private client: Ably.Realtime;
private channel: Ably.RealtimeChannel;
private llm: MockLLM;

constructor(ablyKey: string, channelName: string) {
this.client = new Ably.Realtime({
key: ablyKey,
clientId: 'ai-agent',
});
this.channel = this.client.channels.get(channelName);
this.llm = new MockLLM();
}

async processPrompt(prompt: string, responseId: string): Promise<void> {
const stream = await this.llm.responses.create(prompt);

for await (const event of stream) {
if (event.type === 'message_start') {
// Publish response start
this.channel.publish({
name: 'start',
data: {},
extras: {
headers: {
responseId,
},
},
});
} else if (event.type === 'message_delta') {
// Publish tokens
this.channel.publish({
name: 'token',
data: {
token: event.text,
},
extras: {
headers: {
responseId,
},
},
});
} else if (event.type === 'message_stop') {
// Publish response stop
this.channel.publish({
name: 'stop',
data: {},
extras: {
headers: {
responseId,
},
},
});
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const config = {
ABLY_KEY: import.meta.env.VITE_ABLY_KEY || 'YOUR_ABLY_KEY_HERE',
};
49 changes: 49 additions & 0 deletions examples/ai-transport-message-per-token/javascript/src/llm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Mock LLM Service
// This simulates a generic LLM SDK with streaming capabilities

interface StreamEvent {
type: 'message_start' | 'message_delta' | 'message_stop';
text?: string;
responseId: string;
}

export class MockLLM {
private readonly responseText =
'Ably AI Transport is a solution for building stateful, steerable, multi-device AI experiences into new or existing applications. You can use AI Transport as the transport layer with any LLM or agent framework, without rebuilding your existing stack or being locked to a particular vendor.';

responses = {
create: (prompt: string) => this.createStream(prompt),
};

private async *createStream(_prompt: string): AsyncIterable<StreamEvent> {
const responseId = `resp_${crypto.randomUUID()}`;

// Yield start event
yield { type: 'message_start', responseId };

// Chunk text into tokens (simulates LLM tokenization)
const tokens = this.chunkTextLikeAI(this.responseText);

for (const token of tokens) {
// Simulate realistic delay between tokens
await new Promise((resolve) => setTimeout(resolve, Math.random() * 150 + 50));

// Yield token event
yield { type: 'message_delta', text: token, responseId };
}

// Yield stop event
yield { type: 'message_stop', responseId };
}

private chunkTextLikeAI(text: string): string[] {
const chunks: string[] = [];
let pos = 0;
while (pos < text.length) {
const size = Math.floor(Math.random() * 8) + 1;
chunks.push(text.slice(pos, pos + size));
pos += size;
}
return chunks.filter((chunk) => chunk.length > 0);
}
}
123 changes: 123 additions & 0 deletions examples/ai-transport-message-per-token/javascript/src/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as Ably from 'ably';
import { Agent } from './agent';
import { config } from './config';

// Generate unique channel name for this session
const CHANNEL_NAME = `ai-transport-${crypto.randomUUID()}`;
const client = new Ably.Realtime({
key: config.ABLY_KEY,
});
const channel = client.channels.get(CHANNEL_NAME);
const responseTextElement = document.getElementById('response-text') as HTMLDivElement;
const connectionToggle = document.getElementById('connection-toggle') as HTMLButtonElement;
const promptButton = document.getElementById('prompt-button') as HTMLButtonElement;
const processingStatus = document.getElementById('processing-status') as HTMLSpanElement;

let currentResponseId: string | null = null;
let responseCompleted = false;
let responseText = '';
let isHydrating = false;
let pendingTokens: string[] = [];

const updateDisplay = () => {
responseTextElement.innerText = responseText;
processingStatus.innerText = responseCompleted ? 'Completed' : 'In Progress';
};

channel.subscribe('start', (message) => {
const responseId = message.extras?.headers?.responseId;
if (responseId && currentResponseId === responseId) {
responseCompleted = false;
responseText = '';
pendingTokens = [];
updateDisplay();
}
});

channel.subscribe('token', (message) => {
const responseId = message.extras?.headers?.responseId;
if (responseId && currentResponseId === responseId) {
if (isHydrating) {
pendingTokens.push(message.data.token);
} else {
responseText += message.data.token;
updateDisplay();
}
}
});

channel.subscribe('stop', (message) => {
const responseId = message.extras?.headers?.responseId;
if (responseId && currentResponseId === responseId) {
responseCompleted = true;
updateDisplay();
}
});

// Hydrate from history after reattaching
const hydrateFromHistory = async () => {
if (!currentResponseId) {
isHydrating = false;
return;
}

let page = await channel.history({ untilAttach: true });

const historyTokens: string[] = [];
while (page) {
for (const message of page.items) {
const responseId = message.extras?.headers?.responseId;
if (responseId !== currentResponseId) {
continue;
}
if (message.name === 'token') {
historyTokens.push(message.data.token);
} else if (message.name === 'stop') {
responseCompleted = true;
}
}
page = page.hasNext() ? await page.next() : null;
}

// History arrives newest-first, so reverse it
// Then append any tokens that arrived during hydration
responseText = historyTokens.reverse().join('') + pendingTokens.join('');
isHydrating = false;
updateDisplay();
};

const handlePromptClick = () => {
currentResponseId = `request-${crypto.randomUUID()}`;
responseText = '';
updateDisplay();
const agent = new Agent(config.ABLY_KEY, CHANNEL_NAME);
agent.processPrompt('What is Ably AI Transport?', currentResponseId);
};

const handleConnect = async () => {
// Set hydrating before attach to buffer any live tokens
isHydrating = true;
pendingTokens = [];

await channel.attach();
await hydrateFromHistory();

connectionToggle.innerText = 'Disconnect';
};

const handleDisconnect = async () => {
await channel.detach();
processingStatus.innerText = 'Paused';
connectionToggle.innerText = 'Connect';
};

const handleConnectionToggle = () => {
if (channel.state === 'attached') {
handleDisconnect();
} else {
handleConnect();
}
};

connectionToggle.onclick = handleConnectionToggle;
promptButton.onclick = handlePromptClick;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import baseConfig from '../../tailwind.config';
import type { Config } from 'tailwindcss';

const config: Config = {
...baseConfig,
content: ['./src/**/*.{js,ts,tsx}', './index.html'],
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import baseConfig from '../../vite.config';

export default defineConfig({
...baseConfig,
envDir: '../../',
});
Loading