diff --git a/src/data/nav/aitransport.ts b/src/data/nav/aitransport.ts
index 9e1567392f..9298d2da62 100644
--- a/src/data/nav/aitransport.ts
+++ b/src/data/nav/aitransport.ts
@@ -116,6 +116,14 @@ export default {
name: 'LangGraph token streaming - message per response',
link: '/docs/guides/ai-transport/lang-graph-message-per-response',
},
+ {
+ name: 'LangGraph human-in-the-loop',
+ link: '/docs/guides/ai-transport/langgraph-human-in-the-loop',
+ },
+ {
+ name: 'Vercel AI SDK human-in-the-loop',
+ link: '/docs/guides/ai-transport/vercel-human-in-the-loop',
+ },
],
},
],
diff --git a/src/pages/docs/ai-transport/index.mdx b/src/pages/docs/ai-transport/index.mdx
index 383264eafd..bc3f418aa6 100644
--- a/src/pages/docs/ai-transport/index.mdx
+++ b/src/pages/docs/ai-transport/index.mdx
@@ -76,6 +76,12 @@ Use the following guides to get started with the Vercel AI SDK:
image: 'icon-tech-javascript',
link: '/docs/guides/ai-transport/vercel-message-per-token',
},
+ {
+ title: 'Human-in-the-loop',
+ description: 'Implement HITL workflows with tool approval over Ably',
+ image: 'icon-tech-javascript',
+ link: '/docs/guides/ai-transport/vercel-human-in-the-loop',
+ },
]}
@@ -97,6 +103,12 @@ Use the following guides to get started with LangGraph:
image: 'icon-tech-javascript',
link: '/docs/guides/ai-transport/lang-graph-message-per-token',
},
+ {
+ title: 'Human-in-the-loop',
+ description: 'Implement HITL workflows with tool approval over Ably',
+ image: 'icon-tech-javascript',
+ link: '/docs/guides/ai-transport/langgraph-human-in-the-loop',
+ },
]}
diff --git a/src/pages/docs/guides/ai-transport/langgraph-human-in-the-loop.mdx b/src/pages/docs/guides/ai-transport/langgraph-human-in-the-loop.mdx
new file mode 100644
index 0000000000..c6cd0496bd
--- /dev/null
+++ b/src/pages/docs/guides/ai-transport/langgraph-human-in-the-loop.mdx
@@ -0,0 +1,571 @@
+---
+title: "Guide: Implement human-in-the-loop with LangGraph"
+meta_description: "Implement human-in-the-loop workflows with LangGraph agents using Ably for real-time approval requests and role-based authorization."
+meta_keywords: "AI, human in the loop, HITL, LangGraph, LangChain, tool calling, approval workflow, AI transport, Ably, realtime, JWT claims, authorization"
+---
+
+This guide shows you how to implement [human-in-the-loop](/docs/ai-transport/messaging/human-in-the-loop) (HITL) workflows with [LangGraph](https://docs.langchain.com/oss/javascript/langgraph/overview) agents using Ably for real-time approval requests and role-based authorization. Specifically, it demonstrates how to pause agent execution when sensitive tool calls are made, request human approval over Ably channels, and resume execution based on the decision.
+
+Using Ably to coordinate human-in-the-loop workflows enables real-time communication between AI agents and human approvers with reliable message delivery. This approach leverages Ably's [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) for role-based authorization, ensuring only authorized users can approve specific actions.
+
+
+
+## Prerequisites
+
+To follow this guide, you need:
+- Node.js 20 or higher
+- An Anthropic API key
+- An Ably API key
+
+Useful links:
+- [LangGraph documentation](https://docs.langchain.com/oss/javascript/langgraph/overview)
+- [LangGraph tool calling](https://js.langchain.com/docs/how_to/tool_calling)
+- [Ably JavaScript SDK getting started](/docs/getting-started/javascript)
+
+Create a new NPM package, which will contain the agent, client, and server code:
+
+
+```shell
+mkdir ably-langgraph-hitl && cd ably-langgraph-hitl
+npm init -y
+```
+
+
+Install the required packages using NPM:
+
+
+```shell
+npm install @langchain/langgraph@^0.2 @langchain/anthropic@^0.3 @langchain/core@^0.3 ably@^2 express jsonwebtoken
+```
+
+
+
+
+Export your API keys to the environment:
+
+
+```shell
+export ANTHROPIC_API_KEY="your_anthropic_api_key_here"
+export ABLY_API_KEY="your_ably_api_key_here"
+```
+
+
+## Step 1: Define a tool that requires approval
+
+Create a LangGraph agent with a tool that performs a sensitive action requiring human approval before execution.
+
+Create a new file `agent.mjs` with the following contents:
+
+
+```javascript
+import { ChatAnthropic } from "@langchain/anthropic";
+import { tool } from "@langchain/core/tools";
+import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
+import { ToolNode } from "@langchain/langgraph/prebuilt";
+import * as z from "zod";
+
+// Define a sensitive tool that requires human approval
+const publishBlogPost = tool(
+ async ({ title }) => {
+ // This would publish the blog post in a real application
+ console.log(`Publishing blog post: ${title}`);
+ return `Successfully published blog post: ${title}`;
+ },
+ {
+ name: "publish_blog_post",
+ description: "Publish a blog post to the website. This action requires human approval.",
+ schema: z.object({
+ title: z.string().describe("Title that identifies the blog post in the CMS"),
+ }),
+ }
+);
+
+const tools = [publishBlogPost];
+const toolNode = new ToolNode(tools);
+
+// Initialize the model with tools
+const model = new ChatAnthropic({
+ model: "claude-sonnet-4-5-20250929",
+}).bindTools(tools);
+
+// Define state with message history
+const StateAnnotation = Annotation.Root({
+ messages: Annotation({
+ reducer: (x, y) => x.concat(y),
+ default: () => [],
+ }),
+});
+
+// Agent node that calls the model
+async function agent(state) {
+ const response = await model.invoke(state.messages);
+ return { messages: [response] };
+}
+
+// Determine next step based on tool calls
+function shouldContinue(state) {
+ const lastMessage = state.messages[state.messages.length - 1];
+ if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
+ return "tools";
+ }
+ return END;
+}
+
+// Build and compile the graph
+const graph = new StateGraph(StateAnnotation)
+ .addNode("agent", agent)
+ .addNode("tools", toolNode)
+ .addEdge(START, "agent")
+ .addConditionalEdges("agent", shouldContinue, ["tools", END])
+ .addEdge("tools", "agent");
+
+const app = graph.compile();
+
+export { app, tools };
+```
+
+
+This creates a LangGraph agent with a `publish_blog_post` tool. In the next steps, you'll add human-in-the-loop approval before the tool executes.
+
+## Step 2: Set up Ably for approval requests
+
+Add Ably to the agent to publish approval requests when sensitive tool calls are detected.
+
+Update your `agent.mjs` file to include Ably and the approval flow:
+
+
+```javascript
+import { ChatAnthropic } from "@langchain/anthropic";
+import { tool } from "@langchain/core/tools";
+import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
+import * as z from "zod";
+import Ably from "ably";
+
+// Initialize Ably Realtime client
+const realtime = new Ably.Realtime({
+ key: process.env.ABLY_API_KEY,
+ echoMessages: false,
+});
+
+// Wait for connection to be established
+await realtime.connection.once("connected");
+
+// Create a channel for HITL communication
+const channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}");
+
+// Track pending approval requests
+const pendingApprovals = new Map();
+
+// Tools that require human approval
+const toolsRequiringApproval = ["publish_blog_post"];
+
+// Send a message to request approval for a tool call
+async function requestHumanApproval(toolCall) {
+ const approvalPromise = new Promise((resolve, reject) => {
+ pendingApprovals.set(toolCall.id, { toolCall, resolve, reject });
+ });
+
+ console.log("Sending approval request:", toolCall.name);
+ await channel.publish({
+ name: "approval-request",
+ data: {
+ tool: toolCall.name,
+ arguments: toolCall.args,
+ },
+ extras: {
+ headers: {
+ toolCallId: toolCall.id,
+ },
+ },
+ });
+
+ return approvalPromise;
+}
+```
+
+
+
+
+## Step 3: Process tool calls with approval
+
+Modify the agent to intercept tool calls that require approval and wait for a human decision before execution.
+
+Add the tool execution logic with approval handling to your `agent.mjs` file:
+
+
+```javascript
+// Mock function that would publish the specified blog post
+async function executePublishBlogPost(args) {
+ console.log("Publishing blog post:", args.title);
+ return `Successfully published blog post: ${args.title}`;
+}
+
+// Execute a tool call after receiving approval
+async function executeToolCall(toolCall) {
+ switch (toolCall.name) {
+ case "publish_blog_post":
+ return await executePublishBlogPost(toolCall.args);
+ default:
+ throw new Error(`Unknown tool: ${toolCall.name}`);
+ }
+}
+
+// Process a tool call, requesting approval if needed
+async function processToolCall(toolCall) {
+ if (toolsRequiringApproval.includes(toolCall.name)) {
+ // Wait for human approval before executing
+ await requestHumanApproval(toolCall);
+ }
+
+ // Execute the tool after approval (or immediately if no approval needed)
+ return await executeToolCall(toolCall);
+}
+```
+
+
+## Step 4: Subscribe to approval responses
+
+Set up a subscription to receive approval decisions from human approvers. Verify the approver's role using [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) embedded in the JWT.
+
+Add the approval response handling to your `agent.mjs` file:
+
+
+```javascript
+// Role hierarchy for authorization
+const roleHierarchy = ["editor", "publisher", "admin"];
+
+// Minimum role required to approve each tool
+const approvalPolicies = {
+ publish_blog_post: "publisher",
+};
+
+// Check if the approver's role meets the minimum required role
+function canApprove(approverRole, requiredRole) {
+ const approverLevel = roleHierarchy.indexOf(approverRole);
+ const requiredLevel = roleHierarchy.indexOf(requiredRole);
+ return approverLevel >= requiredLevel;
+}
+
+// Subscribe to approval responses and resolve pending requests
+async function subscribeApprovalResponses() {
+ await channel.subscribe("approval-response", async (message) => {
+ const response = message.data;
+ const toolCallId = message.extras?.headers?.toolCallId;
+ const pending = pendingApprovals.get(toolCallId);
+
+ if (!pending) return;
+
+ const requiredRole = approvalPolicies[pending.toolCall.name];
+
+ // Get the trusted role from the JWT claim
+ const approverRole = message.extras?.userClaim;
+
+ // Verify the approver's role meets the minimum required role
+ if (!canApprove(approverRole, requiredRole)) {
+ pending.reject(
+ new Error(`Approver role '${approverRole}' insufficient for required '${requiredRole}'`)
+ );
+ pendingApprovals.delete(toolCallId);
+ return;
+ }
+
+ // Process the decision
+ if (response.decision === "approved") {
+ pending.resolve(`Action approved by user with role ${approverRole}`);
+ } else {
+ pending.reject(new Error(`Action rejected by user with role ${approverRole}`));
+ }
+
+ pendingApprovals.delete(toolCallId);
+ });
+}
+```
+
+
+
+
+## Step 5: Run the agent with HITL
+
+Complete the agent by adding a function to run it with the human-in-the-loop workflow.
+
+Add the agent execution logic to your `agent.mjs` file:
+
+
+```javascript
+// Define state with message history
+const StateAnnotation = Annotation.Root({
+ messages: Annotation({
+ reducer: (x, y) => x.concat(y),
+ default: () => [],
+ }),
+});
+
+// Initialize the model (without binding tools, we'll handle them manually)
+const model = new ChatAnthropic({
+ model: "claude-sonnet-4-5-20250929",
+}).bindTools([
+ {
+ name: "publish_blog_post",
+ description: "Publish a blog post to the website. This action requires human approval.",
+ schema: z.object({
+ title: z.string().describe("Title that identifies the blog post in the CMS"),
+ }),
+ },
+]);
+
+// Agent node that calls the model
+async function agent(state) {
+ const response = await model.invoke(state.messages);
+ return { messages: [response] };
+}
+
+// Custom tool node that handles approval
+async function toolsWithApproval(state) {
+ const lastMessage = state.messages[state.messages.length - 1];
+ const toolCalls = lastMessage.tool_calls || [];
+ const toolResults = [];
+
+ for (const toolCall of toolCalls) {
+ try {
+ await processToolCall(toolCall);
+ const result = await executeToolCall(toolCall);
+ toolResults.push({
+ tool_call_id: toolCall.id,
+ type: "tool",
+ content: result,
+ });
+ } catch (error) {
+ toolResults.push({
+ tool_call_id: toolCall.id,
+ type: "tool",
+ content: `Error: ${error.message}`,
+ });
+ }
+ }
+
+ return { messages: toolResults };
+}
+
+// Determine next step based on tool calls
+function shouldContinue(state) {
+ const lastMessage = state.messages[state.messages.length - 1];
+ if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
+ return "tools";
+ }
+ return END;
+}
+
+// Build and compile the graph
+const graph = new StateGraph(StateAnnotation)
+ .addNode("agent", agent)
+ .addNode("tools", toolsWithApproval)
+ .addEdge(START, "agent")
+ .addConditionalEdges("agent", shouldContinue, ["tools", END])
+ .addEdge("tools", "agent");
+
+const app = graph.compile();
+
+// Run the agent
+async function runAgent(prompt) {
+ await subscribeApprovalResponses();
+
+ console.log("Running agent with prompt:", prompt);
+
+ const result = await app.invoke({
+ messages: [{ role: "user", content: prompt }],
+ });
+
+ console.log("Agent completed. Final response:");
+ const lastMessage = result.messages[result.messages.length - 1];
+ console.log(lastMessage.content);
+
+ realtime.close();
+}
+
+runAgent("Publish the blog post called 'Ably is awesome'");
+```
+
+
+## Step 6: Create the client for human approvers
+
+Create a client application that human approvers use to receive and respond to approval requests.
+
+Create a new file `client.mjs` with the following contents:
+
+
+```javascript
+import Ably from "ably";
+import readline from "readline";
+
+// Set up readline for user input
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+// Initialize Ably with authentication
+const realtime = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch("http://localhost:3001/api/auth/token");
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+
+realtime.connection.on("connected", () => console.log("Connected to Ably"));
+
+// Create a channel for HITL communication
+const channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}");
+
+// Subscribe to approval requests and prompt for user decision
+await channel.subscribe("approval-request", (message) => {
+ const request = message.data;
+ const toolCallId = message.extras?.headers?.toolCallId;
+
+ console.log("\n--- Approval Request ---");
+ console.log("Tool:", request.tool);
+ console.log("Arguments:", JSON.stringify(request.arguments, null, 2));
+
+ rl.question("Approve this action? (y/n): ", async (answer) => {
+ const decision = answer.toLowerCase() === "y" ? "approved" : "rejected";
+
+ await channel.publish({
+ name: "approval-response",
+ data: {
+ decision,
+ },
+ extras: {
+ headers: {
+ toolCallId,
+ },
+ },
+ });
+
+ console.log(`Decision sent: ${decision}`);
+ });
+});
+
+console.log("Client ready. Waiting for approval requests...");
+```
+
+
+The client authenticates using a JWT that contains the user's role as a claim. When approval requests arrive, the user can approve or reject them, and the decision is published back to the channel.
+
+
+
+## Step 7: Set up the authentication server
+
+Create an authentication server that issues JWTs with role claims for approvers.
+
+Create a new file `server.mjs` with the following contents:
+
+
+```javascript
+import express from "express";
+import jwt from "jsonwebtoken";
+
+const app = express();
+
+// Mock authentication middleware
+// Replace with your actual authentication logic
+function authenticateUser(req, res, next) {
+ // In production, verify user credentials and look up their role
+ req.session = { userId: "user123", role: "publisher" };
+ next();
+}
+
+// Return the claims payload to embed in the signed JWT
+function getJWTClaims(userId, role) {
+ return {
+ "x-ably-clientId": userId,
+ "ably.channel.*": role,
+ };
+}
+
+// Auth endpoint for clients to obtain a signed JWT
+app.get("/api/auth/token", authenticateUser, (req, res) => {
+ const [keyName, keySecret] = process.env.ABLY_API_KEY.split(":");
+
+ // Sign a JWT using the secret part of the Ably API key
+ const token = jwt.sign(
+ getJWTClaims(req.session.userId, req.session.role),
+ keySecret,
+ {
+ algorithm: "HS256",
+ keyid: keyName,
+ expiresIn: "1h",
+ }
+ );
+
+ res.type("application/jwt").send(token);
+});
+
+app.listen(3001, () => {
+ console.log("Auth server running on http://localhost:3001");
+});
+```
+
+
+The server embeds the user's role as a [user claim](/docs/auth/capabilities#custom-restrictions-on-channels-) in the JWT. The `ably.channel.*` claim makes the role available in message extras, allowing the agent to verify authorization.
+
+
+
+## Step 8: Test the HITL workflow
+
+Run all three components to test the complete human-in-the-loop workflow.
+
+Start the authentication server:
+
+
+```shell
+node server.mjs
+```
+
+
+In a separate terminal, start the client:
+
+
+```shell
+node client.mjs
+```
+
+
+In another terminal, run the agent:
+
+
+```shell
+node agent.mjs
+```
+
+
+The workflow proceeds as follows:
+
+1. The agent receives the prompt and calls the `publish_blog_post` tool.
+2. The agent publishes an approval request to the Ably channel.
+3. The client receives the request and prompts the user for a decision.
+4. The user approves or rejects the action.
+5. The client publishes the decision with the user's role in the JWT claims.
+6. The agent verifies the user's role and proceeds accordingly.
+
+## Next steps
+
+- Learn more about the [human-in-the-loop](/docs/ai-transport/messaging/human-in-the-loop) pattern used in this guide
+- Understand [sessions and identity](/docs/ai-transport/sessions-identity) in AI-enabled applications
+- Explore [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) for role-based authorization
+- Learn about [LangGraph streaming](/docs/guides/ai-transport/lang-graph-message-per-response) for token-by-token delivery
diff --git a/src/pages/docs/guides/ai-transport/vercel-human-in-the-loop.mdx b/src/pages/docs/guides/ai-transport/vercel-human-in-the-loop.mdx
new file mode 100644
index 0000000000..c008bcee47
--- /dev/null
+++ b/src/pages/docs/guides/ai-transport/vercel-human-in-the-loop.mdx
@@ -0,0 +1,522 @@
+---
+title: "Guide: Implement human-in-the-loop with Vercel AI SDK"
+meta_description: "Implement human-in-the-loop workflows with the Vercel AI SDK using Ably for real-time approval requests and role-based authorization."
+meta_keywords: "AI, human in the loop, HITL, Vercel AI SDK, tool calling, approval workflow, AI transport, Ably, realtime, JWT claims, authorization"
+---
+
+This guide shows you how to implement [human-in-the-loop](/docs/ai-transport/messaging/human-in-the-loop) (HITL) workflows with the [Vercel AI SDK](https://ai-sdk.dev/docs) using Ably for real-time approval requests and role-based authorization. Specifically, it demonstrates how to pause tool execution when sensitive actions are requested, route approval requests to human approvers over Ably channels, and proceed based on the decision.
+
+Using Ably to coordinate human-in-the-loop workflows enables real-time communication between AI agents and human approvers with reliable message delivery. This approach leverages Ably's [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) for role-based authorization, ensuring only authorized users can approve specific actions.
+
+
+
+## Prerequisites
+
+To follow this guide, you need:
+- Node.js 20 or higher
+- A Vercel AI Gateway API key (or an API key for your preferred provider)
+- An Ably API key
+
+Useful links:
+- [Vercel AI SDK documentation](https://ai-sdk.dev/docs)
+- [Vercel AI SDK tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling)
+- [Ably JavaScript SDK getting started](/docs/getting-started/javascript)
+
+Create a new NPM package, which will contain the agent, client, and server code:
+
+
+```shell
+mkdir ably-vercel-hitl && cd ably-vercel-hitl
+npm init -y
+```
+
+
+Install the required packages using NPM:
+
+
+```shell
+npm install ai@^6 ably@^2 express jsonwebtoken zod
+```
+
+
+
+
+Export your API keys to the environment:
+
+
+```shell
+export AI_GATEWAY_API_KEY="your_ai_gateway_api_key_here"
+export ABLY_API_KEY="your_ably_api_key_here"
+```
+
+
+## Step 1: Define tools with approval requirements
+
+Create an agent with tools that require human approval before execution. The Vercel AI SDK allows you to define tools and control their execution flow.
+
+Create a new file `agent.mjs` with the following contents:
+
+
+```javascript
+import { generateText, tool } from "ai";
+import { z } from "zod";
+
+// Define tools - some require approval, others don't
+const tools = {
+ // This tool executes automatically (no approval needed)
+ getDraftPosts: tool({
+ description: "Get a list of draft blog posts from the CMS",
+ parameters: z.object({}),
+ execute: async () => {
+ return ["Draft 1: Introduction to AI", "Draft 2: Ably is awesome"];
+ },
+ }),
+
+ // This tool requires human approval (no execute function)
+ publishBlogPost: tool({
+ description: "Publish a blog post to the website. This action requires human approval.",
+ parameters: z.object({
+ title: z.string().describe("Title that identifies the blog post in the CMS"),
+ }),
+ // No execute function - requires manual handling
+ }),
+};
+
+// Track which tools require approval
+const toolsRequiringApproval = ["publishBlogPost"];
+
+export { tools, toolsRequiringApproval };
+```
+
+
+Tools without an `execute` function will not run automatically. Instead, they return their parameters, allowing you to implement approval logic before execution.
+
+## Step 2: Set up Ably for approval requests
+
+Add Ably to the agent to publish approval requests when sensitive tool calls are detected.
+
+Update your `agent.mjs` file to include Ably and the approval flow:
+
+
+```javascript
+import { generateText, tool } from "ai";
+import { z } from "zod";
+import Ably from "ably";
+
+// Initialize Ably Realtime client
+const realtime = new Ably.Realtime({
+ key: process.env.ABLY_API_KEY,
+ echoMessages: false,
+});
+
+// Wait for connection to be established
+await realtime.connection.once("connected");
+
+// Create a channel for HITL communication
+const channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}");
+
+// Track pending approval requests
+const pendingApprovals = new Map();
+
+// Define tools
+const tools = {
+ getDraftPosts: tool({
+ description: "Get a list of draft blog posts from the CMS",
+ parameters: z.object({}),
+ execute: async () => {
+ return ["Draft 1: Introduction to AI", "Draft 2: Ably is awesome"];
+ },
+ }),
+
+ publishBlogPost: tool({
+ description: "Publish a blog post to the website. This action requires human approval.",
+ parameters: z.object({
+ title: z.string().describe("Title that identifies the blog post in the CMS"),
+ }),
+ }),
+};
+
+// Track which tools require approval
+const toolsRequiringApproval = ["publishBlogPost"];
+
+// Send a message to request approval for a tool call
+async function requestHumanApproval(toolCall) {
+ const approvalPromise = new Promise((resolve, reject) => {
+ pendingApprovals.set(toolCall.toolCallId, { toolCall, resolve, reject });
+ });
+
+ console.log("Sending approval request:", toolCall.toolName);
+ await channel.publish({
+ name: "approval-request",
+ data: {
+ tool: toolCall.toolName,
+ arguments: toolCall.args,
+ },
+ extras: {
+ headers: {
+ toolCallId: toolCall.toolCallId,
+ },
+ },
+ });
+
+ return approvalPromise;
+}
+```
+
+
+
+
+## Step 3: Subscribe to approval responses
+
+Set up a subscription to receive approval decisions from human approvers. Verify the approver's role using [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) embedded in the JWT.
+
+Add the approval response handling to your `agent.mjs` file:
+
+
+```javascript
+// Role hierarchy for authorization
+const roleHierarchy = ["editor", "publisher", "admin"];
+
+// Minimum role required to approve each tool
+const approvalPolicies = {
+ publishBlogPost: "publisher",
+};
+
+// Check if the approver's role meets the minimum required role
+function canApprove(approverRole, requiredRole) {
+ const approverLevel = roleHierarchy.indexOf(approverRole);
+ const requiredLevel = roleHierarchy.indexOf(requiredRole);
+ return approverLevel >= requiredLevel;
+}
+
+// Subscribe to approval responses and resolve pending requests
+async function subscribeApprovalResponses() {
+ await channel.subscribe("approval-response", async (message) => {
+ const response = message.data;
+ const toolCallId = message.extras?.headers?.toolCallId;
+ const pending = pendingApprovals.get(toolCallId);
+
+ if (!pending) return;
+
+ const requiredRole = approvalPolicies[pending.toolCall.toolName];
+
+ // Get the trusted role from the JWT claim
+ const approverRole = message.extras?.userClaim;
+
+ // Verify the approver's role meets the minimum required role
+ if (!canApprove(approverRole, requiredRole)) {
+ pending.reject(
+ new Error(`Approver role '${approverRole}' insufficient for required '${requiredRole}'`)
+ );
+ pendingApprovals.delete(toolCallId);
+ return;
+ }
+
+ // Process the decision
+ if (response.decision === "approved") {
+ pending.resolve(`Action approved by user with role ${approverRole}`);
+ } else {
+ pending.reject(new Error(`Action rejected by user with role ${approverRole}`));
+ }
+
+ pendingApprovals.delete(toolCallId);
+ });
+}
+```
+
+
+
+
+## Step 4: Execute tools with approval
+
+Add logic to execute tools that require approval. After receiving approval, execute the actual tool logic.
+
+Add the tool execution logic to your `agent.mjs` file:
+
+
+```javascript
+// Execute the actual tool logic after approval
+async function executeToolAfterApproval(toolCall) {
+ switch (toolCall.toolName) {
+ case "publishBlogPost":
+ console.log("Publishing blog post:", toolCall.args.title);
+ return `Successfully published blog post: ${toolCall.args.title}`;
+ default:
+ throw new Error(`Unknown tool: ${toolCall.toolName}`);
+ }
+}
+
+// Process tool calls that require approval
+async function processToolCallsWithApproval(toolCalls) {
+ const toolResults = [];
+
+ for (const toolCall of toolCalls) {
+ if (toolsRequiringApproval.includes(toolCall.toolName)) {
+ try {
+ // Request and wait for approval
+ await requestHumanApproval(toolCall);
+
+ // Execute the tool after approval
+ const result = await executeToolAfterApproval(toolCall);
+ toolResults.push({
+ toolCallId: toolCall.toolCallId,
+ result,
+ });
+ } catch (error) {
+ toolResults.push({
+ toolCallId: toolCall.toolCallId,
+ result: `Error: ${error.message}`,
+ });
+ }
+ }
+ }
+
+ return toolResults;
+}
+```
+
+
+## Step 5: Run the agent with HITL
+
+Complete the agent by adding a function to run it with the human-in-the-loop workflow. The agent uses a loop to handle multi-step tool calling with approval.
+
+Add the agent execution logic to your `agent.mjs` file:
+
+
+```javascript
+async function runAgent(prompt) {
+ await subscribeApprovalResponses();
+
+ console.log("Running agent with prompt:", prompt);
+
+ let messages = [{ role: "user", content: prompt }];
+ let continueLoop = true;
+
+ while (continueLoop) {
+ const response = await generateText({
+ model: "openai/gpt-4o",
+ messages,
+ tools,
+ maxSteps: 1, // Process one step at a time for HITL
+ });
+
+ // Check for tool calls that need approval
+ const pendingToolCalls = response.steps
+ .flatMap((step) => step.toolCalls)
+ .filter((tc) => toolsRequiringApproval.includes(tc.toolName));
+
+ if (pendingToolCalls.length > 0) {
+ // Process tools requiring approval
+ const toolResults = await processToolCallsWithApproval(pendingToolCalls);
+
+ // Add the tool results to the conversation
+ messages = [
+ ...messages,
+ { role: "assistant", content: response.text, toolCalls: pendingToolCalls },
+ ...toolResults.map((tr) => ({
+ role: "tool",
+ toolCallId: tr.toolCallId,
+ content: tr.result,
+ })),
+ ];
+ } else if (response.finishReason === "tool-calls") {
+ // Automatic tools were called, continue the loop
+ messages = response.messages;
+ } else {
+ // No more tool calls, we're done
+ continueLoop = false;
+ console.log("\nAgent completed. Final response:");
+ console.log(response.text);
+ }
+ }
+
+ realtime.close();
+}
+
+runAgent("Publish the blog post called 'Ably is awesome'");
+```
+
+
+## Step 6: Create the client for human approvers
+
+Create a client application that human approvers use to receive and respond to approval requests.
+
+Create a new file `client.mjs` with the following contents:
+
+
+```javascript
+import Ably from "ably";
+import readline from "readline";
+
+// Set up readline for user input
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+});
+
+// Initialize Ably with authentication
+const realtime = new Ably.Realtime({
+ authCallback: async (tokenParams, callback) => {
+ try {
+ const response = await fetch("http://localhost:3001/api/auth/token");
+ const token = await response.text();
+ callback(null, token);
+ } catch (error) {
+ callback(error, null);
+ }
+ },
+});
+
+realtime.connection.on("connected", () => console.log("Connected to Ably"));
+
+// Create a channel for HITL communication
+const channel = realtime.channels.get("ai:{{RANDOM_CHANNEL_NAME}}");
+
+// Subscribe to approval requests and prompt for user decision
+await channel.subscribe("approval-request", (message) => {
+ const request = message.data;
+ const toolCallId = message.extras?.headers?.toolCallId;
+
+ console.log("\n--- Approval Request ---");
+ console.log("Tool:", request.tool);
+ console.log("Arguments:", JSON.stringify(request.arguments, null, 2));
+
+ rl.question("Approve this action? (y/n): ", async (answer) => {
+ const decision = answer.toLowerCase() === "y" ? "approved" : "rejected";
+
+ await channel.publish({
+ name: "approval-response",
+ data: {
+ decision,
+ },
+ extras: {
+ headers: {
+ toolCallId,
+ },
+ },
+ });
+
+ console.log(`Decision sent: ${decision}`);
+ });
+});
+
+console.log("Client ready. Waiting for approval requests...");
+```
+
+
+The client authenticates using a JWT that contains the user's role as a claim. When approval requests arrive, the user can approve or reject them, and the decision is published back to the channel.
+
+
+
+## Step 7: Set up the authentication server
+
+Create an authentication server that issues JWTs with role claims for approvers.
+
+Create a new file `server.mjs` with the following contents:
+
+
+```javascript
+import express from "express";
+import jwt from "jsonwebtoken";
+
+const app = express();
+
+// Mock authentication middleware
+// Replace with your actual authentication logic
+function authenticateUser(req, res, next) {
+ // In production, verify user credentials and look up their role
+ req.session = { userId: "user123", role: "publisher" };
+ next();
+}
+
+// Return the claims payload to embed in the signed JWT
+function getJWTClaims(userId, role) {
+ return {
+ "x-ably-clientId": userId,
+ "ably.channel.*": role,
+ };
+}
+
+// Auth endpoint for clients to obtain a signed JWT
+app.get("/api/auth/token", authenticateUser, (req, res) => {
+ const [keyName, keySecret] = process.env.ABLY_API_KEY.split(":");
+
+ // Sign a JWT using the secret part of the Ably API key
+ const token = jwt.sign(
+ getJWTClaims(req.session.userId, req.session.role),
+ keySecret,
+ {
+ algorithm: "HS256",
+ keyid: keyName,
+ expiresIn: "1h",
+ }
+ );
+
+ res.type("application/jwt").send(token);
+});
+
+app.listen(3001, () => {
+ console.log("Auth server running on http://localhost:3001");
+});
+```
+
+
+The server embeds the user's role as a [user claim](/docs/auth/capabilities#custom-restrictions-on-channels-) in the JWT. The `ably.channel.*` claim makes the role available in message extras, allowing the agent to verify authorization.
+
+
+
+## Step 8: Test the HITL workflow
+
+Run all three components to test the complete human-in-the-loop workflow.
+
+Start the authentication server:
+
+
+```shell
+node server.mjs
+```
+
+
+In a separate terminal, start the client:
+
+
+```shell
+node client.mjs
+```
+
+
+In another terminal, run the agent:
+
+
+```shell
+node agent.mjs
+```
+
+
+The workflow proceeds as follows:
+
+1. The agent receives the prompt and determines it needs to call the `publishBlogPost` tool.
+2. The agent publishes an approval request to the Ably channel.
+3. The client receives the request and prompts the user for a decision.
+4. The user approves or rejects the action.
+5. The client publishes the decision with the user's role in the JWT claims.
+6. The agent verifies the user's role and proceeds accordingly.
+
+## Next steps
+
+- Learn more about the [human-in-the-loop](/docs/ai-transport/messaging/human-in-the-loop) pattern used in this guide
+- Understand [sessions and identity](/docs/ai-transport/sessions-identity) in AI-enabled applications
+- Explore [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) for role-based authorization
+- Learn about [Vercel AI SDK streaming](/docs/guides/ai-transport/vercel-message-per-response) for token-by-token delivery