From 618703481d970f030f7d1ba115363dc8cf0cbaad Mon Sep 17 00:00:00 2001 From: matt423 Date: Fri, 30 Jan 2026 11:30:51 +0000 Subject: [PATCH 1/2] Add progress updates documentation for tool calls Add comprehensive documentation for streaming progress updates during long-running tool execution, addressing AIT-312. Changes: - Add "Progress updates" section to tool-calls.mdx - Document progress updates via Messages (discrete events) - Document progress updates via LiveObjects (state synchronization) - Include complete setup and initialization examples - Provide guidance on choosing between approaches - Show combined approach using both Messages and LiveObjects The documentation covers: - Publishing progress updates with toolCallId correlation - Subscribing to progress updates on the client - LiveObjects setup with LiveCounter and LiveMap - Use cases for each approach - Complete code examples with proper imports --- .../ai-transport/messaging/tool-calls.mdx | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/src/pages/docs/ai-transport/messaging/tool-calls.mdx b/src/pages/docs/ai-transport/messaging/tool-calls.mdx index 9a00922a28..22d01554b9 100644 --- a/src/pages/docs/ai-transport/messaging/tool-calls.mdx +++ b/src/pages/docs/ai-transport/messaging/tool-calls.mdx @@ -592,6 +592,282 @@ channel.subscribe("tool_result", message -> { ``` +## Progress updates + +Some tool calls take significant time to complete, such as processing large files, performing complex calculations, or executing multi-step operations. For long-running tools, streaming progress updates to users provides visibility into execution status and improves the user experience by showing that work is actively happening. + +You can deliver progress updates using two approaches: + +- Messages: Best for discrete status updates and milestone events +- LiveObjects: Best for continuous numeric progress and shared state synchronization + +### Progress updates via messages + +Publish progress messages to the channel as the tool executes, using the `toolCallId` to correlate progress updates with the specific tool call: + + +```javascript +const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}'); + +// Publish initial tool call +await channel.publish({ + name: 'tool_call', + data: { + name: 'process_document', + args: { documentId: 'doc_123', pages: 100 } + }, + extras: { + headers: { + responseId: 'resp_abc123', + toolCallId: 'tool_456' + } + } +}); + +// Publish progress updates as tool executes +await channel.publish({ + name: 'tool_progress', + data: { + name: 'process_document', + status: 'Processing page 25 of 100', + percentComplete: 25 + }, + extras: { + headers: { + responseId: 'resp_abc123', + toolCallId: 'tool_456' + } + } +}); + +// Continue publishing progress as work progresses +await channel.publish({ + name: 'tool_progress', + data: { + name: 'process_document', + status: 'Processing page 75 of 100', + percentComplete: 75 + }, + extras: { + headers: { + responseId: 'resp_abc123', + toolCallId: 'tool_456' + } + } +}); + +// Publish final result +await channel.publish({ + name: 'tool_result', + data: { + name: 'process_document', + result: { processedPages: 100, summary: 'Document processed successfully' } + }, + extras: { + headers: { + responseId: 'resp_abc123', + toolCallId: 'tool_456' + } + } +}); +``` + + +Subscribe to progress updates on the client by listening for the `tool_progress` message type: + + +```javascript +const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}'); + +// Track tool execution progress +const toolProgress = new Map(); + +await channel.subscribe((message) => { + const { responseId, toolCallId } = message.extras?.headers || {}; + + switch (message.name) { + case 'tool_call': + toolProgress.set(toolCallId, { + name: message.data.name, + status: 'Starting...', + percentComplete: 0 + }); + renderProgressBar(toolCallId, 0); + break; + + case 'tool_progress': + const progress = toolProgress.get(toolCallId); + if (progress) { + progress.status = message.data.status; + progress.percentComplete = message.data.percentComplete; + renderProgressBar(toolCallId, message.data.percentComplete); + } + break; + + case 'tool_result': + toolProgress.delete(toolCallId); + renderCompleted(toolCallId, message.data.result); + break; + } +}); +``` + + +Message-based progress is useful for: + +- Step-by-step status descriptions +- Milestone notifications +- Workflow stages with distinct phases +- Audit trails requiring discrete event records + +### Progress updates via LiveObjects + +Use [LiveObjects](/docs/liveobjects) for state-based progress tracking. LiveObjects provides a shared data layer where progress state is automatically synchronized across all subscribed clients, making it ideal for continuous progress tracking. + +Use [LiveCounter](/docs/liveobjects/counter) for numeric progress values like completion percentages or item counts. Use [LiveMap](/docs/liveobjects/map) to track complex progress state with multiple fields. + +First, import and initialize the LiveObjects plugin: + + +```javascript +import * as Ably from 'ably'; +import { LiveObjects, LiveMap, LiveCounter } from 'ably/liveobjects'; + +// Initialize client with LiveObjects plugin +const realtime = new Ably.Realtime({ + key: '{{API_KEY}}', + plugins: { LiveObjects } +}); + +// Get channel with LiveObjects capabilities +const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}', { + modes: ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH'] +}); + +// Get the channel's LiveObjects root +const root = await channel.object.get(); +``` + + +Create a LiveMap to track tool progress: + + +```javascript +// Create a LiveMap to track tool progress +await root.set('tool_456_progress', LiveMap.create({ + status: 'starting', + itemsProcessed: LiveCounter.create(0), + totalItems: 100, + currentItem: '' +})); + +// Update progress as tool executes +const progress = root.get('tool_456_progress'); + +await progress.set('status', 'processing'); +await progress.set('currentItem', 'item_25'); +await progress.get('itemsProcessed').increment(25); + +// Continue updating as work progresses +await progress.set('currentItem', 'item_75'); +await progress.get('itemsProcessed').increment(50); + +// Final increment to reach 100% +await progress.set('currentItem', 'item_100'); +await progress.get('itemsProcessed').increment(25); + +// Mark complete +await progress.set('status', 'completed'); +``` + + +Subscribe to LiveObjects updates on the client to render realtime progress: + + +```javascript +import * as Ably from 'ably'; +import { LiveObjects } from 'ably/liveobjects'; + +// Initialize client with LiveObjects plugin +const realtime = new Ably.Realtime({ + key: '{{API_KEY}}', + plugins: { LiveObjects } +}); + +// Get channel with LiveObjects capabilities +const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}', { + modes: ['OBJECT_SUBSCRIBE', 'OBJECT_PUBLISH'] +}); + +// Get the channel's LiveObjects root +const root = await channel.object.get(); + +// Subscribe to progress updates +const progress = root.get('tool_456_progress'); +progress.subscribe(() => { + const status = progress.get('status').value(); + const itemsProcessed = progress.get('itemsProcessed').value(); + const totalItems = progress.get('totalItems').value(); + const percentComplete = Math.round((itemsProcessed / totalItems) * 100); + + renderProgressBar('tool_456', percentComplete, status); +}); +``` + + + + +LiveObjects-based progress is useful for: + +- Continuous progress bars with frequent updates +- Distributed tool execution across multiple workers +- Complex progress state with multiple fields +- Scenarios where multiple agents or processes contribute to the same progress counter + +### Choosing the right approach + +Choose messages when: + +- Progress updates are infrequent (every few seconds or at specific milestones) +- You need a complete audit trail of all progress events +- Progress information is descriptive text rather than numeric +- Each update represents a distinct event or stage transition + +Choose LiveObjects when: + +- Progress updates are frequent (multiple times per second) +- You're tracking numeric progress like percentages or counts +- Multiple processes or workers contribute to the same progress counter +- You want to minimize message overhead for high-frequency updates + +You can combine both approaches for comprehensive progress tracking. Use LiveObjects for high-frequency numeric progress and messages for important milestone notifications: + + +```javascript +// Update numeric progress continuously via LiveObjects +await progress.get('itemsProcessed').increment(1); + +// Publish milestone messages at key points +if (itemsProcessed === totalItems / 2) { + await channel.publish({ + name: 'tool_progress', + data: { + name: 'process_document', + status: 'Halfway complete - 50 of 100 items processed' + }, + extras: { + headers: { + responseId: 'resp_abc123', + toolCallId: 'tool_456' + } + } + }); +} +``` + + ## Human-in-the-loop workflows Tool calls resolved by humans are one approach to implementing human-in-the-loop workflows. When an agent encounters a tool call that needs human resolution, it publishes the tool call to the channel and waits for the human to publish the result back over the channel. From f0d322d7b1c2d36ee0d716e94b3659e36982d9e8 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 2 Feb 2026 11:35:40 +0000 Subject: [PATCH 2/2] feat: add Python and Java examples for tool progress and result updates in messaging docs --- .../ai-transport/messaging/tool-calls.mdx | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/src/pages/docs/ai-transport/messaging/tool-calls.mdx b/src/pages/docs/ai-transport/messaging/tool-calls.mdx index 22d01554b9..4004b459b1 100644 --- a/src/pages/docs/ai-transport/messaging/tool-calls.mdx +++ b/src/pages/docs/ai-transport/messaging/tool-calls.mdx @@ -671,6 +671,140 @@ await channel.publish({ } }); ``` +```python +channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}') + +# Publish initial tool call +await channel.publish(Message( + name='tool_call', + data={ + 'name': 'process_document', + 'args': {'documentId': 'doc_123', 'pages': 100} + }, + extras={ + 'headers': { + 'responseId': 'resp_abc123', + 'toolCallId': 'tool_456' + } + } +)) + +# Publish progress updates as tool executes +await channel.publish(Message( + name='tool_progress', + data={ + 'name': 'process_document', + 'status': 'Processing page 25 of 100', + 'percentComplete': 25 + }, + extras={ + 'headers': { + 'responseId': 'resp_abc123', + 'toolCallId': 'tool_456' + } + } +)) + +# Continue publishing progress as work progresses +await channel.publish(Message( + name='tool_progress', + data={ + 'name': 'process_document', + 'status': 'Processing page 75 of 100', + 'percentComplete': 75 + }, + extras={ + 'headers': { + 'responseId': 'resp_abc123', + 'toolCallId': 'tool_456' + } + } +)) + +# Publish final result +await channel.publish(Message( + name='tool_result', + data={ + 'name': 'process_document', + 'result': {'processedPages': 100, 'summary': 'Document processed successfully'} + }, + extras={ + 'headers': { + 'responseId': 'resp_abc123', + 'toolCallId': 'tool_456' + } + } +)) +``` +```java +Channel channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +// Helper method to create message extras with headers +MessageExtras createExtras(String responseId, String toolCallId) { + JsonObject extrasJson = new JsonObject(); + JsonObject headers = new JsonObject(); + headers.addProperty("responseId", responseId); + headers.addProperty("toolCallId", toolCallId); + extrasJson.add("headers", headers); + return new MessageExtras(extrasJson); +} + +// Publish initial tool call +JsonObject toolCallData = new JsonObject(); +toolCallData.addProperty("name", "process_document"); +JsonObject args = new JsonObject(); +args.addProperty("documentId", "doc_123"); +args.addProperty("pages", 100); +toolCallData.add("args", args); + +Message toolCall = new Message( + "tool_call", + toolCallData.toString(), + createExtras("resp_abc123", "tool_456") +); +channel.publish(toolCall); + +// Publish progress updates as tool executes +JsonObject progress1 = new JsonObject(); +progress1.addProperty("name", "process_document"); +progress1.addProperty("status", "Processing page 25 of 100"); +progress1.addProperty("percentComplete", 25); + +Message progressMsg1 = new Message( + "tool_progress", + progress1.toString(), + createExtras("resp_abc123", "tool_456") +); +channel.publish(progressMsg1); + +// Continue publishing progress as work progresses +JsonObject progress2 = new JsonObject(); +progress2.addProperty("name", "process_document"); +progress2.addProperty("status", "Processing page 75 of 100"); +progress2.addProperty("percentComplete", 75); + +Message progressMsg2 = new Message( + "tool_progress", + progress2.toString(), + createExtras("resp_abc123", "tool_456") +); +channel.publish(progressMsg2); + +// Publish final result +JsonObject resultData = new JsonObject(); +resultData.addProperty("name", "process_document"); +JsonObject result = new JsonObject(); +result.addProperty("processedPages", 100); +result.addProperty("summary", "Document processed successfully"); +resultData.add("result", result); + +Message resultMsg = new Message( + "tool_result", + resultData.toString(), + createExtras("resp_abc123", "tool_456") +); +channel.publish(resultMsg); +``` Subscribe to progress updates on the client by listening for the `tool_progress` message type: @@ -711,6 +845,86 @@ await channel.subscribe((message) => { } }); ``` +```python +channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}') + +# Track tool execution progress +tool_progress = {} + +async def handle_message(message): + headers = message.extras.get('headers', {}) if message.extras else {} + response_id = headers.get('responseId') + tool_call_id = headers.get('toolCallId') + + if message.name == 'tool_call': + tool_progress[tool_call_id] = { + 'name': message.data.get('name'), + 'status': 'Starting...', + 'percentComplete': 0 + } + render_progress_bar(tool_call_id, 0) + + elif message.name == 'tool_progress': + progress = tool_progress.get(tool_call_id) + if progress: + progress['status'] = message.data.get('status') + progress['percentComplete'] = message.data.get('percentComplete') + render_progress_bar(tool_call_id, message.data.get('percentComplete')) + + elif message.name == 'tool_result': + if tool_call_id in tool_progress: + del tool_progress[tool_call_id] + render_completed(tool_call_id, message.data.get('result')) + +# Subscribe to all messages on the channel +await channel.subscribe(handle_message) +``` +```java +Channel channel = realtime.channels.get("{{RANDOM_CHANNEL_NAME}}"); + +// Track tool execution progress +Map toolProgress = new HashMap<>(); + +// Subscribe to all messages on the channel +channel.subscribe(message -> { + JsonObject headers = message.extras != null + ? message.extras.asJsonObject().getAsJsonObject("headers") + : null; + + String responseId = headers != null && headers.has("responseId") + ? headers.get("responseId").getAsString() + : null; + String toolCallId = headers != null && headers.has("toolCallId") + ? headers.get("toolCallId").getAsString() + : null; + + switch (message.name) { + case "tool_call": + JsonObject newProgress = new JsonObject(); + newProgress.addProperty("name", ((JsonObject) message.data).get("name").getAsString()); + newProgress.addProperty("status", "Starting..."); + newProgress.addProperty("percentComplete", 0); + toolProgress.put(toolCallId, newProgress); + renderProgressBar(toolCallId, 0); + break; + + case "tool_progress": + JsonObject progress = toolProgress.get(toolCallId); + if (progress != null) { + JsonObject progressData = (JsonObject) message.data; + progress.addProperty("status", progressData.get("status").getAsString()); + progress.addProperty("percentComplete", progressData.get("percentComplete").getAsInt()); + renderProgressBar(toolCallId, progressData.get("percentComplete").getAsInt()); + } + break; + + case "tool_result": + toolProgress.remove(toolCallId); + renderCompleted(toolCallId, ((JsonObject) message.data).get("result")); + break; + } +}); +``` Message-based progress is useful for: