Files
supabase/apps/studio/lib/ai/message-utils.ts
T
Matt Rossman c54168842b fix(assistant): prevent parallel approval-required tool calls (#47008)
Addresses the issue of parallel tool approvals freezing Assistant and
causing UX ambiguity around what happens if only part of multiple
dependent invocations is approved.

Two layers at which this is addressed:
1. Tightens prompt to clarify approval tools must be issued one per
step, not in parallel.
2. In case something slips past the prompt, this also auto-denies all
but the first `approval-required` tool call when the model issues
multiple in the same step, so the model is forced to reissue them
sequentially.


**Demo**

The following chats demo me explicitly asking Assistant to run those
approval-gated tools in parallel, and the Assistant correctly invokes
them sequentially instead.

| Parallel `execute_sql` request | Parallel `deploy_edge_function`
request |
|--------|--------|
| <img width="1820" height="4240" alt="CleanShot 2026-06-16 at 16 20
23@2x"
src="https://github.com/user-attachments/assets/3ddad61d-24d6-4e5b-8572-c261755f3a03"
/> | <img width="1738" height="2726" alt="CleanShot 2026-06-16 at 16 22
59@2x"
src="https://github.com/user-attachments/assets/77a0aa20-e3cb-4061-b270-ab2a96d1e64a"
/> |


As shown in [this
trace](https://www.braintrust.dev/app/supabase.io/p/Assistant/trace?object_type=project_logs&object_id=5a8d02e5-b3b6-40cc-ba76-ecee286478f4&r=a3a37857-95df-4a95-a5a8-818ea305b2a5&s=a3a37857-95df-4a95-a5a8-818ea305b2a5),
parallel tool calls are still allowed for context gathering tools that
don't require approval:

<img width="1051" height="517" alt="CleanShot 2026-06-16 at 16 25 42@2x"
src="https://github.com/user-attachments/assets/00d208b1-6131-4b8e-910e-e92fd2a79a5b"
/>


Closes AI-803

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Improvements**
* Approval-required operations are now processed sequentially rather
than in parallel within the AI Assistant.

* **Tests**
* Added comprehensive test coverage for the parallel approval prevention
logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-16 17:20:06 -04:00

40 lines
1.5 KiB
TypeScript

import { isToolUIPart, type UIMessage } from 'ai'
/**
* Prepares messages for API transmission by cleaning and limiting history
*/
export function prepareMessagesForAPI(messages: UIMessage[]): UIMessage[] {
// [Joshen] Specifically limiting the chat history that get's sent to reduce the
// size of the context that goes into the model. This should always be an odd number
// as much as possible so that the first message is always the user's
const MAX_CHAT_HISTORY = 7
const slicedMessages = messages.slice(-MAX_CHAT_HISTORY)
// Filter out results from messages before sending to the model
const cleanedMessages = slicedMessages.map((_message) => {
const message = _message as UIMessage & { results?: unknown }
const cleanedMessage = { ...message } as UIMessage & { results?: unknown }
if (message.role === 'assistant' && message.results) {
delete cleanedMessage.results
}
return cleanedMessage as UIMessage
})
return cleanedMessages
}
/**
* Returns approval IDs to auto-deny when the model issues multiple approval-required
* tool calls in the same turn — all but the first, so the model reissues them sequentially.
*/
export function getParallelApprovalIdsToReject(messages: UIMessage[]): string[] {
const lastMessage = messages.findLast((m) => m.role === 'assistant')
if (!lastMessage) return []
const pendingIds = (lastMessage.parts ?? []).flatMap((part) =>
isToolUIPart(part) && part.state === 'approval-requested' ? [part.approval.id] : []
)
return pendingIds.slice(1)
}