Conversation
|
@zayedadel Does it mean that it re-prompts tool results as UserMessage? In case you store messages in DB, will there be 2 User Messages in sequence? Or the first user message (which invoked the tool call) get lost? |
yes it does unfortunately, as for the db storage quirk it can be mitigated or refactored into a better solution, however i wanted to get this pr up first to see if the maintainers approve of the general 'human-in-the-loop' concept and approach before over-engineering the message storage state or enforce my own opinions. |
|
@zayedadel This is would be a great addition. I certainly have a need for HITL for something I am currently working on. Considering that Laravel has been really leaning into annotations, have you considered perhaps instead of doing: class DeleteUserFiles implements Tool
{
public function requiresApproval(): bool
{
return true;
}
// ...
}doing this instead? #[RequiresApproval]
class DeleteUserFiles implements Tool
{
// ...
} |
|
@grahamsutton yeah i actually went that way but i had to use reflection and i didn't like that so i chose to avoid it , none the less it'll be very easy to add , i just wanted the pr up to see if it is going anywhere before overthinking it , lets see what @taylorotwell have in mind for it then proceed with that. |
|
Hey @zayedadel, How does this work in multiple tool call chains? For example, if you instruct the AI to “save an image, rename a folder, compress it,” how would it proceed if the second action requires confirmation? If I understand this correctly, since this only re-prompts the LLM, the first action will be executed, the second one will be executed but return a “pending” status, and the third one will also be executed. After user confirmation, the second action will be executed. However, at that point, the folder has already been compressed (with an incorrect name). In summary, I think this might become unreliable in the case of tool chains. To achieve true human-in-the-loop flows, I believe the tool should not return a message until confirmation is received. This would unfortunately require blocking the tool’s response until confirmation is obtained (for example, by checking for a given condition until a certain timeout). |
|
@nicodevs Because I am throwing a ToolApprovalRequiredException from inside the tool's closure, PHP immediately halts execution and unwinds the stack. So if the LLM requests [Tool 1, Tool 2 (needs approval), Tool 3]: Tool 1 executes. You are absolutely right about the resulting unreliability, though. When the developer calls approve() for Tool 2 later, the agent is re-prompted. The LLM usually realizes it still needs to execute Tool 3 based on the conversation history, but we are essentially forcing the LLM to 're-plan' the rest of the chain, which is definitely fragile. Given the current asynchronous/stateless constraints of typical PHP web requests, do you think this stateless 're-prompt' workaround is acceptable for a first iteration (perhaps with a note in the docs about tool chains), or we will have to hold off until a true blocking/pausing mechanism can be engineered at the Prism level. |
|
I see, thank you so much for explaining @zayedadel
In a side project, I am currently using Redis blpop to block execution until receiving a confirmation: use Illuminate\Support\Facades\Redis;
$key = 'foo';
$timeout = 120;
askUserForConfirmation();
$reply = Redis::blpop($key, $timeout);
if (!$reply) {
return 'Confirmation timed out';
}
if (strtolower((string) $reply[1]) === 'yes') {
return executeSomething();
}
return 'User did not confirm execution';In this example, |
[0.x] Introduce Human-in-the-Loop Tool Approval
Summary
This PR introduces a fully opt-in, stateless mechanism for requiring human approval before specific tools are executed.
As AI agents are given more destructive or sensitive capabilities (deleting records, sending emails, generating payments), developers need a straightforward way to interrupt the agent's execution loop, present the pending action to a human, and then easily resume execution upon approval or rejection.
How it Works
The implementation leans heavily into Laravel's existing conventions, avoiding completely the need for new dependencies or modifications to the underlying
Prismpackage.1. Tools opt-in via a
requiresApproval()method:2. The Prompt returns a
PendingApprovalResponseinstead of executing:If an agent attempts to call an opted-in tool, the execution loop is gracefully intercepted via an internal
ToolApprovalRequiredException. The prompt call returns aPendingApprovalResponsecontaining the pending tool calls, rather than an executed text/structured response.3. Developers approve (or reject) to resume from where the agent paused:
Using the new
HasApprovalFlowtrait, agents gainapprove()andreject()methods that execute the tool (if approved) and dynamically re-prompt the agent with the result or rejection reason.Known Limitations
Because
laravel/aicurrently leverages Prism's internal synchronous step loop, it cannot literally "pause" a process mid-flight and resume it from memory later. Thus, callingapprove()orreject()relies on re-prompting the agent in a new API call with the tool's result injected into the conversation.This works perfectly, but incurs an extra LLM API call. I believe this is an acceptable trade-off for the simplicity and power of human-in-the-loop flows without needing background workers natively.
Next Steps / Discussion:
Currently, the developer is responsible for storing the
PendingToolCallbetween requests (e.g., in the session or cache). If you feellaravel/aishould prescribe a default persistence mechanism (similar to howRemembersConversationsopinionates a DB schema), I would be happy to build that out in a follow-up commit or PR.Tests
Added comprehensive Feature tests asserting the interception flow, event dispatching, and conditional toggling. Zero regressions on the existing unit and integration suite.