Confirmations
Introduction
Some tool operations should not run silently. Deleting a file, deleting an email, dropping a calendar event — the agent calling these unprompted is the kind of mistake that ends in tears. Laraclaw ships a confirmation flow that any tool can opt into: the tool asks, the connector relays the prompt to the user, the user replies "yes", and the tool runs.
This is not a separate tool you call. It's a contract a tool declares against an operation, and a Redis-backed wait-for-reply loop the connector implements.
Which Connectors Support It
A connector opts in by implementing Laraclaw\Connectors\Contracts\SupportsConfirmation. The package ships three connectors that do:
| Connector | Supports confirmations |
|---|---|
| Telegram | Yes |
| Slack | Yes |
| Yes | |
| API | No |
| Terminal | No |
If a tool tries to ask for confirmation on a connector that does not implement the contract, the tool simply runs the operation without asking. The contract is the gate.
How a Tool Opts In
Tools that extend BaseTool declare their confirmation prompts in a $requiresApproval map. Keys are operation names, values are the prompt template:
class FileManager extends BaseTool
{
protected array $requiresApproval = [
'delete' => 'Delete the file at {path} on disk {disk}?',
];
}
The placeholders in curly braces are replaced with the matching parameter from the tool's request before the prompt is sent. Before each operation, BaseTool::confirmOperation() checks the map; if the operation is in there, it asks the connector to run the confirmation flow before dispatching the actual handler.
What the User Sees
The connector posts the prompt back to the same conversation, prefixed with a warning emoji:
⚠️ Delete the file at reports/old.csv on disk local? Reply 'Yes' to confirm.
The user has 120 seconds (the default timeout) to reply. The reply must equal yes (case-insensitive, first non-empty line of the message — so an email reply with quoted history below still matches). Any other reply, or a timeout, counts as a no.
How It Works Under the Hood
The flow is implemented by the ChecksRedisForConfirmations trait, which every supporting connector uses:
- The tool calls
$connector->askForConfirmation($message, $prompt). - The trait sets a Redis key
awaiting_confirm:{connector}:{thread_key}with the timeout as TTL. - It posts the prompt back to the user through the connector.
- It opens a
BLPOPagainstconfirm:{connector}:{thread_key}on a dedicated Redis connection (registered withread_write_timeout = -1so the socket never closes mid-wait). - When the user's reply arrives at the webhook, the connector's
intercept()method sees the awaiting key in Redis, pushes the reply text into the confirm key, and short-circuits normal processing. - The blocked tool wakes up, reads the reply, returns
trueif it wasyes,falseotherwise. - The
finallyblock deletes both Redis keys regardless of how the wait ended.
The dedicated laraclaw-blocking Redis connection is registered automatically by the service provider. There is nothing to configure on your end — but Redis must be running, even if your queue driver is something else.
Edge Cases
- Timeout. If 120 seconds pass with no reply, both Redis keys are deleted and the tool gets
false. The user can still reply later, but the message will be processed as a normal message, not a confirmation. - Wrong connector replies. Confirmation keys are scoped by
(connector, thread_key). A "yes" sent in Slack cannot satisfy a pending Telegram confirmation. - The queue worker dies mid-wait. Both Redis keys have a TTL equal to the timeout, so they expire on their own. No leaks. The tool's job will be retried by the queue if it was retriable.
- The user replies with a file and no caption. The message is consumed (so it does not also reach the agent), but the wait stays open. Effectively a silent no.
- Multiple confirmations stacked up. Only one confirmation can be pending per
(connector, thread_key)at a time. A secondaskForConfirmation()call overwrites the first awaiting key.
Tool Authors: Choosing What to Confirm
Confirm operations that are destructive, irreversible, or expensive. Don't confirm read-only operations. Don't confirm small operations to be safe — the friction trains the user to type "yes" without reading. Save it for things where a wrong call would actually hurt: deletes, sends, anything that touches money or external systems.
protected array $requiresApproval = [
'delete' => 'Delete {path}?',
'send' => 'Send this email to {to}?',
'delete_folder' => 'Delete the folder {folder} and everything in it?',
];