Skip to content

Commit 1665b75

Browse files
committed
ait/hitl: align with gist developed for guide
SEE: #3133 (review)
1 parent e8ae3b4 commit 1665b75

1 file changed

Lines changed: 64 additions & 32 deletions

File tree

src/pages/docs/ai-transport/messaging/human-in-the-loop.mdx

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,30 @@ Human-in-the-loop authorization follows a request-approval pattern over Ably cha
3232

3333
## Request human approval <a id="request"/>
3434

35-
When an agent identifies an action requiring human oversight, it publishes a request to the channel. The request should include sufficient context for the approver to make an informed decision. The `requestId` enables correlation between requests and responses when handling multiple concurrent approval flows.
35+
When an agent identifies an action requiring human oversight, it publishes a request to the channel. The request should include sufficient context for the approver to make an informed decision. The `toolCallId` in the message [extras](/docs/messages#properties) enables correlation between requests and responses when handling multiple concurrent approval flows.
36+
37+
The agent stores each pending request in some local state before publishing. When an approval response arrives, the agent uses the `toolCallId` to retrieve the original tool call details, verify the approver's permissions for that specific action, execute the tool if approved, and resolve the pending approval.
3638

3739
<Code>
3840
```javascript
39-
const channel = ably.channels.get('{{RANDOM_CHANNEL_NAME}}');
41+
const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}');
42+
const pendingApprovals = new Map();
4043

4144
async function requestHumanApproval(toolCall) {
42-
const requestId = crypto.randomUUID();
43-
44-
await channel.publish('approval-request', {
45-
requestId: requestId,
46-
action: toolCall.name,
47-
parameters: toolCall.parameters
45+
pendingApprovals.set(toolCall.id, { toolCall });
46+
47+
await channel.publish({
48+
name: 'approval-request',
49+
data: {
50+
name: toolCall.name,
51+
arguments: toolCall.arguments
52+
},
53+
extras: {
54+
headers: {
55+
toolCallId: toolCall.id
56+
}
57+
}
4858
});
49-
50-
return requestId;
5159
}
5260
```
5361
</Code>
@@ -58,7 +66,7 @@ Set [`echoMessages`](/docs/api/realtime-sdk/types#client-options) to `false` on
5866

5967
## Review and decide <a id="review"/>
6068

61-
Authorized humans subscribe to approval requests on the conversation channel and publish their decisions. The `requestId` correlates the response with the original request.
69+
Authorized humans subscribe to approval requests on the conversation channel and publish their decisions. The `toolCallId` correlates the response with the original request.
6270

6371
Use [identified clients](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-identity) or [user claims](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) to establish a verified identity or role for the approver. For example, when a user [authenticates with Ably](/docs/ai-transport/sessions-identity/identifying-users-and-agents#authenticating), embed their identity and role in the JWT:
6472

@@ -79,25 +87,40 @@ For more information about establishing verified identities and roles, see [Iden
7987

8088
<Code>
8189
```javascript
82-
const channel = ably.channels.get('{{RANDOM_CHANNEL_NAME}}');
90+
const channel = realtime.channels.get('{{RANDOM_CHANNEL_NAME}}');
8391

8492
await channel.subscribe('approval-request', (message) => {
8593
const request = message.data;
94+
const toolCallId = message.extras?.headers?.toolCallId;
8695
// Display request for human review
87-
displayApprovalUI(request);
96+
displayApprovalUI(request, toolCallId);
8897
});
8998

90-
async function approve(requestId) {
91-
await channel.publish('approval-response', {
92-
requestId: requestId,
93-
decision: 'approved'
99+
async function approve(toolCallId) {
100+
await channel.publish({
101+
name: 'approval-response',
102+
data: {
103+
decision: 'approved'
104+
},
105+
extras: {
106+
headers: {
107+
toolCallId: toolCallId
108+
}
109+
}
94110
});
95111
}
96112

97-
async function reject(requestId) {
98-
await channel.publish('approval-response', {
99-
requestId: requestId,
100-
decision: 'rejected'
113+
async function reject(toolCallId) {
114+
await channel.publish({
115+
name: 'approval-response',
116+
data: {
117+
decision: 'rejected'
118+
},
119+
extras: {
120+
headers: {
121+
toolCallId: toolCallId
122+
}
123+
}
101124
});
102125
}
103126
```
@@ -109,15 +132,15 @@ Set [`echoMessages`](/docs/api/realtime-sdk/types#client-options) to `false` in
109132
110133
## Process the decision <a id="process"/>
111134
112-
The agent listens for human decisions and acts accordingly. When a response arrives, the agent retrieves the pending request using the `requestId`, verifies that the user is permitted to approve that specific action, and either executes the action or handles the rejection.
135+
The agent listens for human decisions and acts accordingly. When a response arrives, the agent retrieves the pending request using the `toolCallId`, verifies that the user is permitted to approve that specific action, and either executes the action or handles the rejection.
113136
114137
<Aside data-type="note">
115138
For audit trails, use [integration rules](/docs/integrations) to stream approval messages to external systems.
116139
</Aside>
117140
118141
### Verify by user identity <a id="verify-identity"/>
119142
120-
Use the `clientId` to identify the approver and look up their permissions in your database or user management system. This approach is useful when permissions are managed externally or change frequently.
143+
Use the `clientId` to identify the approver and look up their permissions in your database or access control system. This approach is useful when permissions are managed externally or change frequently.
121144
122145
<Aside data-type="note">
123146
This approach requires the user to authenticate as an [identified client](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-identity) with a verified `clientId`.
@@ -129,11 +152,12 @@ const pendingApprovals = new Map();
129152

130153
await channel.subscribe('approval-response', async (message) => {
131154
const response = message.data;
132-
const pending = pendingApprovals.get(response.requestId);
155+
const toolCallId = message.extras?.headers?.toolCallId;
156+
const pending = pendingApprovals.get(toolCallId);
133157

134158
if (!pending) return;
135159

136-
// The clientId is verified by Ably - this is the trusted approver identity
160+
// The clientId is the trusted approver identity
137161
const approverId = message.clientId;
138162

139163
// Look up user-specific permissions from your database
@@ -151,7 +175,7 @@ await channel.subscribe('approval-response', async (message) => {
151175
console.log(`Action rejected by ${approverId}`);
152176
}
153177

154-
pendingApprovals.delete(response.requestId);
178+
pendingApprovals.delete(toolCallId);
155179
});
156180
```
157181
</Code>
@@ -164,11 +188,15 @@ Use [user claims](/docs/auth/capabilities#custom-restrictions-on-channels-) to e
164188
This approach uses [authenticated claims for users](/docs/ai-transport/sessions-identity/identifying-users-and-agents#user-claims) to embed custom claims in JWTs that represent user roles or attributes.
165189
</Aside>
166190
167-
Different actions may require different authorization levels - for example, a user might approve low-value purchases, a manager might approve purchases up to a certain limit, while an admin can approve any purchase amount. When an approval arrives, compare the approver's role against the minimum required role for that action type:
191+
Different actions may require different authorization levels. For example, an editor might be able to create drafts for review, but only a publisher or admin can approve publishing a blog post. Define approval policies that map tool names to minimum required roles, and when an approval arrives, compare the approver's role against the required role for that action type:
168192
169193
<Code>
170194
```javascript
171-
const roleHierarchy = ['user', 'manager', 'admin'];
195+
const roleHierarchy = ['editor', 'publisher', 'admin'];
196+
197+
const approvalPolicies = {
198+
publish_blog_post: 'publisher'
199+
};
172200

173201
function canApprove(approverRole, requiredRole) {
174202
const approverLevel = roleHierarchy.indexOf(approverRole);
@@ -180,15 +208,19 @@ function canApprove(approverRole, requiredRole) {
180208
// When processing approval response
181209
await channel.subscribe('approval-response', async (message) => {
182210
const response = message.data;
183-
const pending = pendingApprovals.get(response.requestId);
211+
const toolCallId = message.extras?.headers?.toolCallId;
212+
const pending = pendingApprovals.get(toolCallId);
213+
214+
if (!pending) return;
215+
184216
const policy = approvalPolicies[pending.toolCall.name];
185217

186218
// Get the trusted role from the JWT claim
187219
const approverRole = message.extras?.userClaim;
188220

189221
// Verify the approver's role meets the minimum required role for this action
190-
if (!canApprove(approverRole, policy.minRole)) {
191-
console.log(`Approver role '${approverRole}' insufficient for required '${policy.minRole}'`);
222+
if (!canApprove(approverRole, policy)) {
223+
console.log(`Approver role '${approverRole}' insufficient: minimum required role is '${policy}'`);
192224
return;
193225
}
194226

@@ -199,7 +231,7 @@ await channel.subscribe('approval-response', async (message) => {
199231
console.log(`Action rejected by role ${approverRole}`);
200232
}
201233

202-
pendingApprovals.delete(response.requestId);
234+
pendingApprovals.delete(toolCallId);
203235
});
204236
```
205237
</Code>

0 commit comments

Comments
 (0)