get-convex / get-convex/agent

Batch tool-approval responses: one tool message per step for many approvals

Open
#354 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.