Every AI agent that touches something real eventually needs a human in the loop. Someone has to approve the delete, confirm the wire transfer, or just say "go ahead" before a tool call runs. The question BoxLang AI 3.4.0 answers is: approve it how? A terminal prompt? A webhook? A Slack button?
The answer is the new Gateway SPI, IGateway, a single interface that normalizes every one of those into the same shape.
What a gateway actually is
A gateway is a bidirectional adapter. In one direction, it turns a platform event, a keystroke, an HTTP POST, a button click, into normalized agent input. In the other, it turns agent events, including a suspended approval request, back into something native to that platform.
Every gateway implements IGateway. Every capability method has a safe default, so a gateway only needs to override what it actually supports:
// Capabilities a gateway can declare
inboundMessages, outboundMessages, streaming, threads,
attachments, messageEditing, interactiveActions,
humanApproval, argumentEditing, authentication
A gateway that only handles approvals doesn't need to implement streaming. One that only streams doesn't need to know what argument editing is. Nobody pays for capabilities they don't use.
Resolving a gateway
The aiGateway() BIF resolves a gateway by name, core or externally registered:
cli = aiGateway( "cli" )
http = aiGateway( "http", { secret: "shared-hmac-secret" } )
Three gateways ship in the box:
| Gateway | What it does |
|---|---|
CliGateway | Blocking stdin/stdout approval prompt, now with approve_always/approve_session, not just approve/reject/quit |
HttpGateway | HMAC-SHA256 signed requests, timestamp tolerance + nonce dedup, TTL-bounded pending interactions, atomic decision claims |
MockGateway | In-memory gateway for tests and examples |
That last point on HttpGateway matters more than it sounds. A duplicate decision POST for an interaction that's already resolved gets rejected outright, not silently overwritten. Two people can't race to approve or reject the same request and have the second write clobber the first.
We also have tons of new gateways that are coming on our new agent framework to be released soon: BxAgents from WhatsApp, Signal, Slack, Discourse, Email, Telegram, etc.
Attaching a gateway to an agent
Gateways plug straight into HumanInTheLoopMiddleware:
aiAgent(
middleware : new HumanInTheLoopMiddleware( gateway: aiGateway( "http" ) ),
checkpointer: aiMemory( "cache" )
)
Swap aiGateway( "http" ) for aiGateway( "cli" ) in a local dev script and nothing else in your agent code changes. The approval flow, the middleware, the checkpointer, none of it knows or cares which gateway is behind it.
Building your own
External platforms, Slack, Discord, Teams, whatever your team runs on, ship as their own modules and register a gateway instance under their own name:
aiGatewayRegistry().register( new MyPlatformGateway(), "my-platform" )
myGateway = aiGateway( "my-platform" )
aiGateway() can also auto-register the instance it constructs, so you don't have to call the registry yourself:
myGateway = aiGateway( name: "http", register: true, module: "my-module" )
To implement IGateway yourself, you get lifecycle hooks for free by extending BaseGateway, which tracks isRunning() and fires onGatewayConnect/onGatewayDisconnect automatically. Override onStart()/onStop(), never start()/stop() directly.
Why this matters
Before 3.4.0, "how does a human approve this" was a decision baked into your middleware code, tied to whatever transport you happened to build first. Now it's a resolvable object. Write your HITL logic once, then decide at deploy time whether approvals come in over a terminal, a signed webhook, or a chat platform your team hasn't built yet.
Next in the series: what happens on the other side of that gateway, the fully rebuilt Human-in-the-Loop subsystem, and the durable grants that mean nobody gets asked the same question twice.
Add Your Comment