Blog

Luis Majano

September 05, 2026

Spread the word


Share your thoughts

HumanInTheLoopMiddleware used to do everything itself: decide which tool calls needed approval, present the request, and wait for a decision. In 3.4.0, that logic has been pulled apart into a real subsystem, and the piece developers will feel the most is that a human's "always allow this" now actually means always.

The extraction

HITL now lives in a dedicated models/hitl/ package with two clear responsibilities split apart:

  • An IApprovalPolicy decides whether a given tool call needs a human to sign off.
  • A HumanInteractionCoordinator owns presenting the request through an attached gateway and resolving the decision.

HumanInTheLoopMiddleware is now a thin adapter over the two. The simplest case still looks exactly like before:

import bxModules.bxai.models.middleware.core.HumanInTheLoopMiddleware;

// Match by tool name, the default policy
hitl = new HumanInTheLoopMiddleware( toolsRequiringApproval: [ "deleteRecord" ] )

But now you can swap in any policy that implements IApprovalPolicy:

hitl = new HumanInTheLoopMiddleware(
    policy : new RiskLevelApprovalPolicy( minLevel: "high" ),
    gateway: aiGateway( "http" )
)

agent = aiAgent( middleware: [ hitl ], checkpointer: aiMemory( "cache" ) )

Built-in policies cover the common shapes out of the box: ToolNameApprovalPolicy (the default), RiskLevelApprovalPolicy, AnnotationApprovalPolicy, CallbackApprovalPolicy, and CompositeApprovalPolicy for combining several. Or write your own, an IApprovalPolicy is just a decision function with a name.

Durable grants, the actual headline feature

Before 3.4.0, an "always approve this" decision only lived as long as the process did. Restart your app, and the human gets asked again. That's fixed with a pluggable IDecisionStore:

// Explicit store
store = aiDecisionStore( "jdbc", { datasource: "myDSN", table: "ai_decisions" } )
hitl  = new HumanInTheLoopMiddleware( toolsRequiringApproval: [ "placeOrder" ], decisionStore: store )

Three store implementations ship: cache, jdbc, and file. Or configure the application-wide default once and every HITL middleware in the app shares it:

// boxlang.json
{
  "modules": {
    "bxai": {
      "settings": {
        "hitl": {
          "decisionStore": {
            "provider": "cache",
            "config": {}
          }
        }
      }
    }
  }
}

That last part is by design, not a limitation: an application never attaches more than one HITL middleware at a time, so there's exactly one source of truth for "what has this human already approved."

The five decisions

Every HITL flow resolves to one of: approve, approve_always, approve_session, reject, edit, cancel. approve_always and approve_session are the two that now persist through the decision store instead of evaporating on restart.

Migration, if you're already using HITL

Existing mode: "cli" / mode: "web" configuration keeps working exactly as it did. If you're attaching a specific gateway anyway, prefer gateway: going forward, an unrecognized mode now falls back to a CLI gateway with a console warning instead of silently pretending to be "cli".

// Still works, unchanged
agent = aiAgent(
    tools     : [ deleteRecordTool ],
    middleware: [ new HumanInTheLoopMiddleware( mode: "web", toolsRequiringApproval: [ "deleteRecord" ] ) ],
    checkpointer: aiMemory( "cache" )
)

Why the split matters

Separating "should this be approved" from "how do we ask" means you can change either independently. Swap your approval policy from tool-name matching to risk-level scoring without touching how approvals get presented. Swap your gateway from CLI to HTTP without touching your approval logic. And with durable grants, the humans approving your agents' actions only have to make each decision once.

Next in the series: what happens when a single turn asks for approval on multiple tool calls at once, and why that used to silently drop half of them.

Docs: Middleware, Human-in-the-Loop

Add Your Comment

Recent Entries

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
BxAudio 1.0: Make, Mix, Master and Speak Audio from BoxLang

BxAudio 1.0: Make, Mix, Master and Speak Audio from BoxLang

Ortus Solutions today announces the general availability of BxAudio 1.0, the audio module for BoxLang. Synthesize instruments and drums, arrange them on a beat grid, master to a loudness target, speak text with open license voices, score a video and play sound in a game. All of it from BoxLang, on the JVM. No Python. Nothing native to install.

Luis Majano
Luis Majano
October 20, 2026
BoxLang Under the Lens: Observability Meets AI - Free Webinar

BoxLang Under the Lens: Observability Meets AI - Free Webinar

Your application is slow. Something is failing. Where do you start looking?

On October 28, 2026, at 11:00 AM CDT, join Luis Majano, Ortus Solutions CEO and BoxLang creator, for a live, hands-on look at how observability and AI come together in BoxLang.

BoxLang Under the Lens: Observability Meets AI introduces BxLens, Lensy, and the additional monitoring capabilities of BoxLang+....

Victor Campos
Victor Campos
October 19, 2026