Examples / Human-in-the-Loop (HITL) / Agent Driven Approval
Agent Driven Approval
Has SpecsRequires API Keys
An AI agent that dynamically decides when to request human approval and constructs context-aware approval messages. This demonstrates **agent-driven HITL** where the agent has tools that wrap HITL primitives and uses them intelligently based on context.
Source Code
--[[
Agent-Driven HITL Approval
This example demonstrates agent-driven human-in-the-loop (HITL), where an AI agent
dynamically decides when to request human input and constructs the questions itself.
KEY INNOVATION:
- Traditional HITL: Procedure code hardcodes calls to Human.approve()
- Agent-Driven HITL: Agent has tools that wrap HITL primitives and decides when/how to use them
The agent:
1. Analyzes the deployment request
2. Decides to call the ask_approval tool
3. Constructs a context-aware approval message
4. Waits for human response
5. Proceeds based on approval/rejection
This makes HITL workflows much more flexible and intelligent compared to hardcoded approaches.
REQUIREMENTS:
- Run from Tactus IDE or VSCode extension (HITL modals require UI)
- Running from CLI will execute but cannot display the approval modal
RUN:
tactus run 03-human-in-the-loop/01-agent-driven-approval.tac \
--param app_name="MyApp" \
--param environment="production"
--]]
local done = require("tactus.tools.done")
-- Define HITL approval toolset
-- This wraps the Human.approve() primitive so agents can call it as a tool
Toolset "hitl_approval" {
description = "Tools for requesting human input and approval",
tools = {
{
name = "ask_approval",
description = "Ask the human for deployment approval. Pass a clear message describing what needs approval.",
input = {
message = field.string{required = true, description = "The approval question to ask the human"}
},
handler = function(args)
Log.info("[Agent → Human] Asking for approval: " .. args.message)
-- Use Human.approve() to get approval
local approved = Human.approve({
message = args.message
})
Log.info("[Human → Agent] Approval response: " .. tostring(approved))
return {
approved = approved,
message = "Human " .. (approved and "approved" or "rejected") .. " the request"
}
end
}
}
}
-- Define deployment review agent
-- The agent decides when to call ask_approval based on its instructions
deployment_reviewer = Agent {
model = "openai/gpt-4o-mini",
tool_choice = "required",
system_prompt = [[You are a deployment review assistant.
Your job is to review deployment requests and ask the human for approval.
When you receive a deployment request:
1. Call the ask_approval tool with a clear message about what deployment you need approval for
2. Wait for the human response
3. Call the done tool with the approval result
Important: You MUST call ask_approval exactly once, then call done with the result.]],
tools = {"hitl_approval", done}
}
Procedure {
input = {
app_name = field.string{required = false},
environment = field.string{required = false}
},
output = {
success = field.boolean{required = true},
app_name = field.string{required = true},
environment = field.string{required = true},
approved = field.boolean{required = true},
done_called = field.boolean{required = true}
},
function(input)
print("=== Agent-Driven HITL Example ===\n")
-- Input validation
local app_name = input.app_name or "MyApp"
local environment = input.environment or "production"
print("Deployment request:")
print(" App: " .. app_name)
print(" Environment: " .. environment)
print("")
-- Run the agent (it will call ask_approval tool and done tool)
print("Invoking deployment review agent...")
-- Format the initial message with deployment details
local message = string.format(
"Please review this deployment request:\n\nApp: %s\nEnvironment: %s\n\nCall the ask_approval tool to get human approval.",
app_name,
environment
)
local max_turns = 5
local turn_count = 0
-- Run agent until done is called or max turns reached
while not done.called() and turn_count < max_turns do
turn_count = turn_count + 1
-- Pass message on first turn
if turn_count == 1 then
deployment_reviewer(message)
else
deployment_reviewer()
end
end
print("\nAgent execution complete.")
-- Verify the agent called the ask_approval tool
if not Tool.called("hitl_approval_ask_approval") then
print("\n❌ SPEC VIOLATION: Agent did not call ask_approval tool")
return {
success = false,
reason = "Agent did not ask for approval"
}
end
print("\n✓ Agent successfully used HITL tool")
-- Get the approval result
local approval_result = Tool.last_result("hitl_approval_ask_approval")
local done_result = done.called() and done.last_call() or nil
print("\nApproval result:")
if approval_result then
local function safe_get(obj, key)
if obj == nil then
return nil
end
if type(obj) == "table" then
return obj[key]
end
local ok, val = pcall(function()
return obj[key]
end)
if ok then
return val
end
return nil
end
-- In mocked runs, tool results may be bridged as Python dict-like objects.
-- Use safe_get to avoid KeyError-style exceptions through the bridge.
print(" Approved: " .. tostring(safe_get(approval_result, "approved")))
print(" Message: " .. tostring(safe_get(approval_result, "message")))
end
if done_result then
print("\nDone called with reason: " .. tostring(done_result.args.reason))
end
-- Extract approval status safely
local approved = false
if approval_result then
local ok, val = pcall(function() return approval_result["approved"] end)
if ok and val ~= nil then
approved = val and true or false
elseif type(approval_result) == "table" and approval_result.approved ~= nil then
approved = approval_result.approved and true or false
else
-- In mocked agent runs, tool calls are recorded with a generic result
-- like {tool=..., args=...} (the tool handler is not executed). For
-- this educational example, treat the presence of the ask_approval
-- tool call as an approval in mock mode.
local ok_tool, tool_name = pcall(function() return approval_result["tool"] end)
if ok_tool and tool_name == "hitl_approval_ask_approval" then
approved = true
elseif type(approval_result) == "table" and approval_result.tool == "hitl_approval_ask_approval" then
approved = true
end
end
end
return {
success = true,
app_name = app_name,
environment = environment,
approved = approved,
done_called = done.called()
}
end
}
Specification([[
Feature: Agent-driven HITL approval (mocked)
Scenario: Agent asks for approval and completes
Given the procedure has started
And the input app_name is "MyApp"
And the input environment is "production"
And the agent "deployment_reviewer" responds with "I will ask for approval."
And the agent "deployment_reviewer" calls tool "hitl_approval_ask_approval" with args "{'message': 'Approve deployment of MyApp to production?'}"
And the agent "deployment_reviewer" calls tool "done" with args "{'reason': 'approval complete'}"
When the procedure runs
Then the procedure should complete successfully
And the output success should be true
And the output approved should be true
And the output done_called should be true
]])
Quick Start
Run the example:
$tactus run 03-human-in-the-loop/02-agent-driven-approval.tacTest with mocks:
$tactus test 03-human-in-the-loop/02-agent-driven-approval.tac --mockNote
This example requires API keys. Set your OPENAI_API_KEY environment variable before running.
Explore more examples
Learn Tactus through practical, runnable examples organized by topic.