diff --git a/src/pages/docs/ai-transport/messaging/tool-calls.mdx b/src/pages/docs/ai-transport/messaging/tool-calls.mdx index 9a00922a28..4004b459b1 100644 --- a/src/pages/docs/ai-transport/messaging/tool-calls.mdx +++ b/src/pages/docs/ai-transport/messaging/tool-calls.mdx @@ -592,6 +592,496 @@ 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' + } + } +}); +``` +```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: + + +```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; + } +}); +``` +```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: + +- 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.