Back to 12 Factor Agents

Chapter 10 - Adding Human Approval

workshops/2025-05/sections/10-human-approval/README.md

latest5.8 KB
Original Source

Chapter 10 - Adding Human Approval

Add support for human approval of operations.

for this section, we'll disable the baml logs. You can optionally enable them if you want to see more details.

export BAML_LOG=off

update the server to handle human approvals

  • Import handleNextStep to execute approved actions
  • Add two payload types to distinguish approvals from responses
  • Handle responses and approvals differently in the endpoint
  • Show better error messages when things go wrongs
diff
src/server.ts
 import express from 'express';
-import { Thread, agentLoop } from '../src/agent';
+import { Thread, agentLoop, handleNextStep } from '../src/agent';
 import { ThreadStore } from '../src/state';
 
 });
 
+
+type ApprovalPayload = {
+    type: "approval";
+    approved: boolean;
+    comment?: string;
+}
+
+type ResponsePayload = {
+    type: "response";
+    response: string;
+}
+
+type Payload = ApprovalPayload | ResponsePayload;
+
 // POST /thread/:id/response - Handle clarification response
 app.post('/thread/:id/response', async (req, res) => {
         return res.status(404).json({ error: "Thread not found" });
     }
+
+    const body: Payload = req.body;
+
+    let lastEvent = thread.events[thread.events.length - 1];
+
+    if (thread.awaitingHumanResponse() && body.type === 'response') {
+        thread.events.push({
+            type: "human_response",
+            data: body.response
+        });
+    } else if (thread.awaitingHumanApproval() && body.type === 'approval' && !body.approved) {
+        // push feedback onto the thread
+        thread.events.push({
+            type: "tool_response",
+            data: `user denied the operation with feedback: "${body.comment}"`
+        });
+    } else if (thread.awaitingHumanApproval() && body.type === 'approval' && body.approved) {
+        // approved, run the tool, pushing results onto the thread
+        await handleNextStep(lastEvent.data, thread);
+    } else {
+        res.status(400).json({
+            error: "Invalid request: " + body.type,
+            awaitingHumanResponse: thread.awaitingHumanResponse(),
+            awaitingHumanApproval: thread.awaitingHumanApproval()
+        });
+        return;
+    }
+
     
-    thread.events.push({
-        type: "human_response",
-        data: req.body.message
-    });
-    
     // loop until stop event
     const newThread = await agentLoop(thread);
     store.update(req.params.id, newThread);
 
-    const lastEvent = newThread.events[newThread.events.length - 1];
+    lastEvent = newThread.events[newThread.events.length - 1];
     lastEvent.data.response_url = `/thread/${req.params.id}/response`;
 
<details> <summary>skip this step</summary>
cp ./walkthrough/10-server.ts src/server.ts
</details>

Add a few methods to the agent to handle approvals and responses

diff
src/agent.ts
         `)
     }
+
+    awaitingHumanResponse(): boolean {
+        const lastEvent = this.events[this.events.length - 1];
+        return ['request_more_information', 'done_for_now'].includes(lastEvent.data.intent);
+    }
+
+    awaitingHumanApproval(): boolean {
+        const lastEvent = this.events[this.events.length - 1];
+        return lastEvent.data.intent === 'divide';
+    }
 }
 
                 // response to human, return the thread
                 return thread;
+            case "divide":
+                // divide is scary, return it for human approval
+                return thread;
             case "add":
             case "subtract":
             case "multiply":
-            case "divide":
                 thread = await handleNextStep(nextStep, thread);
         }
<details> <summary>skip this step</summary>
cp ./walkthrough/10-agent.ts src/agent.ts
</details>

Start the server

npx tsx src/server.ts

Test division with approval

curl -X POST http://localhost:3000/thread \

-H "Content-Type: application/json"
-d '{"message":"can you divide 3 by 4"}'

You should see:

{

"thread_id": "2b243b66-215a-4f37-8bc6-9ace3849043b", "events": [ { "type": "user_input", "data": "can you divide 3 by 4" }, { "type": "tool_call", "data": { "intent": "divide", "a": 3, "b": 4, "response_url": "/thread/2b243b66-215a-4f37-8bc6-9ace3849043b/response" } } ] }

reject the request with another curl call, changing the thread ID

curl -X POST 'http://localhost:3000/thread/{thread_id}/response' \

-H "Content-Type: application/json"
-d '{"type": "approval", "approved": false, "comment": "I dont think thats right, use 5 instead of 4"}'

You should see: the last tool call is now "intent":"divide","a":3,"b":5

{

"events": [ { "type": "user_input", "data": "can you divide 3 by 4" }, { "type": "tool_call", "data": { "intent": "divide", "a": 3, "b": 4, "response_url": "/thread/2b243b66-215a-4f37-8bc6-9ace3849043b/response" } }, { "type": "tool_response", "data": "user denied the operation with feedback: "I dont think thats right, use 5 instead of 4"" }, { "type": "tool_call", "data": { "intent": "divide", "a": 3, "b": 5, "response_url": "/thread/1f1f5ff5-20d7-4114-97b4-3fc52d5e0816/response" } } ] }

now you can approve the operation

curl -X POST 'http://localhost:3000/thread/{thread_id}/response' \

-H "Content-Type: application/json"
-d '{"type": "approval", "approved": true}'

you should see the final message includes the tool response and final result!

...

{ "type": "tool_response", "data": 0.5 }, { "type": "done_for_now", "message": "I divided 3 by 6 and the result is 0.5. If you have any more operations or queries, feel free to ask!", "response_url": "/thread/2b469403-c497-4797-b253-043aae830209/response" }