Batch tool-approval responses: one tool message per step for many approvals
- Dominant language
- TypeScript
- Stars
- 349
- Forks
- 92
- Avg merge
- 3d 10h
- Merged PRs (30d)
- 24
Description
## Summary
`agent.approveToolCall` / `agent.denyToolCall` are single-approval helpers. Each call runs `findApprovalContext`, which lists the newest 100 thread messages to locate the request, and then either saves a new `tool` message or patches the step's existing response message with one more `tool-approval-response` part.
That is the right shape for one approval, but a model step can emit many approval-required tool calls at once (we hit 56 `createLocation` calls in one step). Resolving them by calling the helper in a loop inside one Convex mutation means N sequential 100-message scans plus N rewrites of the same growing message document. In production that exceeded Convex's per-function system-operation time budget (`DATABASE_UDF_SYSTEM_TIMEOUT`, 15 s of DB syscall wall clock) and the whole transaction rolled back, so the batch could never be approved.
## Proposal
Add a batch helper, for example `agent.respondToToolCallApprovals(ctx, { threadId, decisions: Array<{ approvalId, approved, reason? }> })`, that:
1. lists the thread once (same newest-first bounded scan as today),
2. groups the decisions by their request (assistant) message,
3. writes one `tool` message per request message containing every `tool-approval-response` part for that step, merging into an existing response message for the step if one exists,
4. returns `{ messageIdByApprovalId }` (or the per-step message ids) so callers can continue generation from the right `promptMessageId`.
This matches the AI SDK's own tool-approval contract, which collects a step's responses and pushes a single tool message (`messages.push({ role: 'tool', content: approvalResponses })`), and it keeps the existing "already handled" detection since responses are still discovered from the scan. `approveToolCall` / `denyToolCall` could become thin wrappers over it.
## Workaround we run today
We reimplemented the batched write in our app by calling `saveMessage` / `updateMessage` with the same `promptMessageId` linkage the helper uses internally. It works, but it duplicates private behaviour of the helper (`findApprovalContext`, the merge rule) that we would rather not own. Happy to send a PR if the shape above is acceptable.
Versions: `@convex-dev/agent` 0.7.1, `ai` 7.0.x.
Contributor guide
Research direction
Start by reading the existing approveToolCall and denyToolCall helpers, especially findApprovalContext and their saveMessage/updateMessage paths. Trace how approval responses are linked to request messages, then implement the proposed batch entry point so each request gets one response message, existing responses merge, and returned message IDs map back to approvals; keep the single-approval helpers compatible.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design, database
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100