Blog

Luis Majano

September 08, 2026

Spread the word


Share your thoughts

Here's a bug that's easy to miss until it bites someone in production: an agent turn asks for two tool calls at once, both need human approval, and only the first one actually suspends. The second one gets silently skipped. Not rejected, not queued, just gone. That's what happened before 3.4.0, and it's fixed now with batched tool-call approvals.

The old behavior

Say a turn asks the model to check the weather and email the result. Both get_weather and send_email require approval:

agent = aiAgent(
    tools       : [ getWeatherTool, sendEmailTool ],
    middleware  : [ new HumanInTheLoopMiddleware( toolsRequiringApproval: [ "get_weather", "send_email" ] ) ],
    checkpointer: aiMemory( "cache" )
)

result = agent.run( "Check the weather in KC and email me the result", {}, { threadId: "t1" } )

Before 3.4.0, that call sequence played out like this:

1. Model returns two tool calls: get_weather, send_email
2. Middleware sees get_weather needs approval -> suspends the run, checkpoint saved
3. send_email never gets evaluated this pass -> not approved, not rejected, not queued
4. Human approves what they were shown (get_weather)
5. agent.resume() finishes the run -> send_email was NEVER presented for approval

The human who clicked "approve" only ever saw get_weather. They had no way to know send_email was sitting in the same turn, unreviewed. That's the bug, silent, not a crash, not a rejection, just a tool call the approval flow never surfaced.

The fix: one checkpoint, the whole batch

In 3.4.0, the same run suspends with both calls visible at once:

result = agent.run( "Check the weather in KC and email me the result", {}, { threadId: "t1" } )

writeDump( result.isSuspended() )  // true
writeDump( result )                // both get_weather and send_email show up as pending
                                    // in this ONE checkpoint, not just the first one

Both calls belong to the same checkpoint. A human reviewing this run sees the full picture before deciding anything, not one call now and a second one that never asked.

Resuming finishes the whole batch directly against the saved assistant message. Nothing that already executed, or already got blocked, runs twice. And critically, there is no replay of the LLM call, the model isn't asked to think through the turn again just because a human took a minute to click approve.

Resolving a batch

A single decision applies uniformly to everything pending, approve the weather check and the email together:

final = agent.resume( "approve", "t1" )
// get_weather runs, then send_email runs, using the model's original arguments for both

Or resolve each call individually, say the human is fine with checking the weather but doesn't want an email sent:

final = agent.resume(
    [
        { decision: "approve" },                    // get_weather -> executes normally
        { decision: "reject", reason: "not needed" } // send_email  -> never executes, agent
                                                       // gets a rejection result for that call instead
    ],
    "t1"
)

The array lines up positionally with the pending calls in the checkpoint. get_weather still runs and returns real weather data to the agent's context; send_email doesn't run at all, the agent sees a rejection in its place and can react accordingly, for example by telling the user the email wasn't sent.

If you're already calling agent.resume() with a single decision string, nothing breaks. That call shape is unchanged, it's just now correct against a multi-call batch instead of only ever seeing the first pending call.

What's happening under the hood

Two new pieces make this possible. AiMiddlewareResult.defer() lets a middleware mark a pending call as "needs a decision" without stopping the provider from scanning the rest of the batch for other calls that might also need review. And a new IAiMiddleware.afterToolBatch( context ) hook fires once per turn, after every tool call in it has been decided, to build the combined suspension, or fall back to per-provider default handling if no middleware participates.

Provider coverage

This is consistent across OpenAI, Claude, Bedrock, and Cohere. Streaming batching currently covers OpenAI and Claude, the only two providers with streaming tool-call support today.

Why it matters

An agent that silently drops half its approval requests isn't just buggy, it's a security gap. A human reviewing "approved: send_email" has no idea get_weather also ran unreviewed in the same turn. Batching closes that gap by making the checkpoint reflect exactly what the turn actually asked for, nothing more and nothing quietly skipped.

Next in the series: the three-phase guardrail stack that defends the rest of the pipeline against prompt injection and data loss.

Docs: Middleware, Human-in-the-Loop Series Part 1 : https://www.ortussolutions.com/blog/boxlang-34-blog-series-part-i-gateways-one-interface-any-platform Series Part 2 : https://www.ortussolutions.com/blog/boxlang-34-blog-series-part-2-revamped-human-in-the-loop-hitl

Add Your Comment

Recent Entries

Your ColdFusion Upgrade Passed. Did Your Business Workflows?

Your ColdFusion Upgrade Passed. Did Your Business Workflows?

The server restarted. The administrator shows the expected version. Your homepage loads, and a few familiar screens look fine.

Would you tell the business the upgrade is complete?

Consider an illustrative scenario: a customer submits an order after an upgrade. The application saves it and displays a confirmation page. But the confirmation email never arrives, and the scheduled process that sends the order to fulfillment has not completed.

From the browser, the transaction...

Maria Jose Herrera
Maria Jose Herrera
October 22, 2026
Updating Adobe ColdFusion isn't always just installing the latest patch

Updating Adobe ColdFusion isn't always just installing the latest patch

Keeping Adobe ColdFusion updated is an important part of maintaining a secure and reliable application environment.

But applying a ColdFusion update is not always as simple as installing the latest patch and restarting the server.

Updates can include security fixes, library upgrades, JVM-related changes, changes to the ColdFusion Administrator, updated packages, bug fixes, and changes in runtime behavior. Most updates will not cause problems for most applications. But when you'r...

Cristobal Escobar
Cristobal Escobar
October 21, 2026
BoxLang AI 3.6: Your Agent Now Has Eyes, Ears, and a Voice

BoxLang AI 3.6: Your Agent Now Has Eyes, Ears, and a Voice

Ortus Solutions today announces BoxLang AI 3.6, the release that connects the AI framework to the rest of the BoxLang productivity platform. Your agents can now drive a real browser via bx-playwright, speak and master audio via bx-audio, stream text-to-speech token by token, talk to any MCP server over stdio — and do all of it with the same fluent BoxLang syntax they always have.

Luis Majano
Luis Majano
October 20, 2026