foojay – a place for friends of OpenJDK https://foojay.io/today/category/boxlang/ a place for friends of OpenJDK Fri, 29 May 2026 15:43:47 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://foojay.io/wp-content/uploads/2020/04/Favicon-3-2-150x150.png foojay – a place for friends of OpenJDK https://foojay.io/today/category/boxlang/ 32 32 Foojay Podcast #97: From Scripting Language to AI Powerhouse: How BoxLang Is Redefining JVM Development https://foojay.io/today/foojay-podcast-97/ https://foojay.io/today/foojay-podcast-97/#respond Mon, 01 Jun 2026 06:57:25 +0000 https://foojay.io/?p=123995 Table of Contents YouTubePodcast AppsGuestsLinksContent BoxLang is a modern dynamic JVM language built for rapid application development. It's 100% Java-interoperable, compiles to JVM bytecode, and deployable anywhere from OS to AWS Lambda to Spring Boot. In this episode, we sit ...

The post Foojay Podcast #97: From Scripting Language to AI Powerhouse: How BoxLang Is Redefining JVM Development appeared first on foojay.

]]>
Table of Contents
YouTubePodcast AppsGuestsLinksContent

BoxLang is a modern dynamic JVM language built for rapid application development. It's 100% Java-interoperable, compiles to JVM bytecode, and deployable anywhere from OS to AWS Lambda to Spring Boot. In this episode, we sit down with Luis Majano (CEO of Ortus Solutions and creator of BoxLang) and Cristobal Escobar (BoxLang community manager) to dig into the wave of innovation that has hit the platform over the past few months.

We cover the BoxLang AI v3 release, a major overhaul that ships multi-agent orchestration with parent-child hierarchies, an AI Skills system based on Anthropic's open standard, MCP server integration (both consuming and serving), a composable middleware layer with six built-in classes including a FlightRecorder for deterministic CI testing, and a unified API spanning 17 AI providers. Luis and Cristobal walk us through the highlights of a 7-part BoxLang AI deep dive series, covering tools, memory systems & RAG, streaming, middleware, and MCP. We also touch on the BoxLang Spring Boot Starter, BoxLings (an interactive TDD/BDD learning platform), and TestBox 7's real-time streaming test runner.

Whether you're a Java developer curious about dynamic JVM languages, an AI engineer looking for a productive alternative to Python-based agent frameworks, or just want to see what the JVM ecosystem can do in 2026, this episode is for you.

YouTube

Podcast Apps

You can listen and subscribe to the Foojay Podcast on:

Guests

Links

Content

00:00 Introduction of topic and guests
01:17 What is BoxLang and how to use it
05:25 Multi-runtime (WASM) with MatchBox, based on Rust
07:00 Combining BoxLang with Spring Boot
10:40 The abstraction approach in BoxLang AI, compared with LangChain4j and others
14:18 Markdown skill files similar to Claude are also used in BoxLang AI
15:21 About the 7-part Foojay BoxLang Deep Dive posts series, agents, event-driven,...
19:28 BoxLang can be used for MCP server and client
23:01 Premium features in BoxLang and building a company on an open-source project
27:52 BoxLings, an interactive learning tool for BoxLang that teaches TDD and BDD
30:25 TestBox 7, real-time streaming test execution and a browser-based IDE
32:58 How to get started with BoxLang?
34:14 How the evolutions in the JVM and Java language influence BoxLang development
39:33 Which article to read first on Foojay about BoxLang?
43:27 More learning resources and ideas for the future and desktop development
48:05 Conclusions

The post Foojay Podcast #97: From Scripting Language to AI Powerhouse: How BoxLang Is Redefining JVM Development appeared first on foojay.

]]>
https://foojay.io/today/foojay-podcast-97/feed/ 0
Free Webinar: Making AI useful for Java developers in Real Applications with BoxLang! https://foojay.io/today/free-webinar-making-ai-useful-for-java-developers-in-real-applications-with-boxlang/ https://foojay.io/today/free-webinar-making-ai-useful-for-java-developers-in-real-applications-with-boxlang/#respond Fri, 29 May 2026 15:43:47 +0000 https://foojay.io/?p=124004 Table of Contents Making AI Useful in Real ApplicationsWhat This Webinar Is AboutWhat You’ll LearnJoin the Ortus Community AI is everywhere right now, but for many development teams, the biggest question is no longer “What is AI?” it’s “How do ...

The post Free Webinar: Making AI useful for Java developers in Real Applications with BoxLang! appeared first on foojay.

]]>

Table of Contents
Making AI Useful in Real ApplicationsWhat This Webinar Is AboutWhat You’ll LearnJoin the Ortus Community


AI is everywhere right now, but for many development teams, the biggest question is no longer “What is AI?” it’s “How do we actually use it in real applications in a secure, practical, and maintainable way?”

That’s exactly what we’ll explore in our upcoming free June webinar:

Making AI Useful in Real Applications

A Practical Guide to Secure and Effective AI Development

Join Bill Reese, Senior Developer at Ortus Solutions, for a practical session focused on bringing AI into real-world applications using BoxLang and modern JVM development patterns.

Webinar Details

  • Date: Friday, June 5th, 2026
  • Time: 11:00 AM CDT
  • Location: Online Event
  • Speaker: Bill Reese, Senior Developer at Ortus Solutions

What This Webinar Is About

AI can unlock powerful new capabilities for applications, but only when it is implemented with the right patterns, architecture, and security mindset.

In this session, Bill will break down the practical side of AI integration, including where AI provides meaningful value, where it may not be the right fit, and how development teams can approach AI features in a way that is secure, flexible, and maintainable over time.

You’ll also get a demo of the AI+ module, giving you a practical look at how BoxLang can help simplify AI integration in real-world applications. This session will also include a sneak peek at some of the tools and approaches Ortus Solutions is building to help developers create secure, flexible, and maintainable AI-powered features.

What You’ll Learn

During this webinar, we’ll cover:

  • Common AI application patterns and use cases
  • How AI fits into enterprise architectures
  • Security and privacy considerations for AI workflows
  • Why provider abstraction matters
  • The role of tools, agents, and pipelines
  • How unified APIs simplify AI development
  • How the AI+ module can support practical AI integration in BoxLang applications

Why Attend?
If your team is exploring AI, planning AI features, or trying to understand how AI fits into your existing applications, this webinar is designed to give you a grounded and practical starting point.

Instead of focusing on hype, this session will help you understand how to think strategically about AI development, how to avoid common implementation pitfalls, and how BoxLang can help reduce complexity when working with modern AI providers and workflows.

Whether you are modernizing existing applications or building something new, you’ll leave with a clearer understanding of how to approach AI in a way that makes sense for real development teams.

REGISTER FOR FREE

Join the Ortus Community

Be part of the movement shaping the future of web development. Stay connected and receive the latest updates on, product launches, tool updates, promo services and much more.

Subscribe to our newsletter for exclusive content.

SUBSCRIBE

Follow Us on Social media and don’t miss any news and updates:

The post Free Webinar: Making AI useful for Java developers in Real Applications with BoxLang! appeared first on foojay.

]]>
https://foojay.io/today/free-webinar-making-ai-useful-for-java-developers-in-real-applications-with-boxlang/feed/ 0
Intro to the BoxLang Formatter https://foojay.io/today/intro-to-the-boxlang-formatter/ https://foojay.io/today/intro-to-the-boxlang-formatter/#respond Thu, 28 May 2026 17:45:09 +0000 https://foojay.io/?p=123985 Table of Contents Recommended Team Workflow You know the drill. Someone opens a PR and half the review comments are about tabs vs spaces, where braces go, or why that one function has its arguments formatted differently from everything else. ...

The post Intro to the BoxLang Formatter appeared first on foojay.

]]>

Table of Contents


You know the drill. Someone opens a PR and half the review comments are about tabs vs spaces, where braces go, or why that one function has its arguments formatted differently from everything else. It's noise. And it's over.

The BoxLang Formatter is here, and it handles all of that for you.

You can find the docs here: https://boxlang.ortusbooks.com/getting-started/ide-tooling/boxlang-formatter

What Is It?

The BoxLang Formatter is a built-in code formatting tool that ships with BoxLang. It enforces consistent style across .bx, .bxs, .bxm, .cfm, .cfc, and .cfs files — automatically.

It's not a linter. It doesn't just complain. It fixes your code, or tells CI to fail when style drift sneaks in.

Getting Started in 60 Seconds

If you have BoxLang installed, you already have the formatter. No extra install needed.

Format everything in your current directory:

boxlang format

That's it. It recurses through your project and rewrites supported files in place.

Want to target a specific path or file?

# A directory
boxlang format --source ./src

# A single file
boxlang format --source ./models/User.bx

Multiple paths at once (v1.14+):

boxlang format --source commands,models,services

Configure Your Style

The formatter works great out of the box with sensible defaults, but you can customize it with a .bxformat.json file in your project root.

Bootstrap one instantly:

boxlang format --initConfig

This drops a starter config in your current directory. From there, tweak what you care about. Here's a minimal example:

{
  "maxLineLength": 120,
  "tabIndent": true,
  "singleQuote": false,
  "braces": {
    "style": "same-line",
    "require_for_single_statement": true
  },
  "operators": {
    "comparison_style": "symbols"
  }
}

You've got control over indentation, line length, brace style, struct/array formatting, operator style, SQL keyword casing, import sorting, and a lot more. Only override what you need — everything else uses sensible defaults.

Lock It Down in CI

This is where it gets really useful. Run the formatter in check mode as a quality gate:

boxlang format --check --source ./
  • Exits 0 if everything is already formatted correctly
  • Exits non-zero if any file has drift

Drop that into your CI pipeline and pull requests with messy formatting simply won't merge. One command, no separate linter needed.

Recommended Team Workflow

  • Developers run boxlang format before pushing
  • CI runs boxlang format --check on every PR
  • PRs that fail must reformat before merge

No more style debates in code review. The formatter wins.

Format on Save in VS Code

If you want formatting to happen automatically as you work, the BoxLang LSP supports experimental format-on-save.

Step 1 - Enable it in .bxlint.json:

{
  "formatting": {
    "experimental": {
      "enabled": true
    }
  }
}

Step 2 - Add this to your VS Code settings.json:

{
  "[boxlang]": {
    "editor.formatOnSave": true
  },
  "[boxlang-template]": {
    "editor.formatOnSave": true
  }
}

Step 3 - Open the Command Palette and run:

  • BoxLang: Select BoxLang Version (pick latest)
  • BoxLang: Select LSP Version (pick latest)
  • Developer: Reload Window

Save a .bx file and it just formats. Local fast feedback, CI enforcement as the source of truth.

Coming from cfformat?

Already using cfformat in your project? Migration is a two-step process, and your existing style intent is preserved.

Step 1 - Convert your config:

boxlang format --convertConfig --source ./

This transforms your .cfformat.json into a .bxformat.json, keeping your rules intact.

Step 2 - Validate with check mode:

boxlang format --check --source ./

See what (if anything) drifted. Run the formatter once in a cleanup commit, then turn on --check in CI and you're done.

A Few Other Handy Options

Preview without rewriting files — pipe output to stdout instead:

boxlang format --overwrite false --source ./handlers/MainHandler.cfc

Exclude directories (v1.14+):

boxlang format --source . --excludes generated,vendor

Use a custom config path:

boxlang format --config ./config/.bxformat.json --source ./

The Bottom Line

Stop spending review cycles on style. The formatter handles it — in your editor, in your pre-commit hook, in CI. One command, consistent output, zero arguments about semicolons ever again.

Go format something:

boxlang format

Questions? Hit us up on Community & Support or open a discussion on the BoxLang repo. We'd love to hear how you're using it.

The post Intro to the BoxLang Formatter appeared first on foojay.

]]>
https://foojay.io/today/intro-to-the-boxlang-formatter/feed/ 0
Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/ https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/#comments Tue, 26 May 2026 10:14:00 +0000 https://foojay.io/?p=123950 Table of Contents The Fluent Builder — jwtNew() The BIF Functions HMAC Sign and Verify RSA Sign and Verify JWE Encryption alg:none Rejection HMAC Minimum Key Lengths (RFC 7518 §3.2) Algorithm Allowlist Clock Skew Tolerance Authentication Middleware Token Refresh with ...

The post Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang appeared first on foojay.

]]>

Table of Contents


JWT authentication is everywhere. But rolling it correctly — with proper algorithm enforcement, key management, clock skew handling, JWE encryption, and zero security footguns — is anything but trivial. Today, we're shipping bx-jwt, a production-ready JWT/JWE module for BoxLang that handles all of it out of the box, so you can focus on building, not fighting cryptography.

bx-jwt is part of the BoxLang+ and BoxLang++ subscription tiers — our enterprise-grade module collection built for teams that take security seriously.

What is bx-jwt?

bx-jwt is a full implementation of the JWT/JWE specification stack for BoxLang:

  • JWS (JSON Web Signature) — HMAC, RSA, and Elliptic Curve signing
  • JWE (JSON Web Encryption) — RSA and symmetric encryption
  • RFC 7518 — JSON Web Algorithms
  • RFC 7519 — JSON Web Token

It ships with two APIs that serve different tastes: a fluent builder for expressive, chainable token construction, and a suite of BIF functions for direct, functional-style usage. Both share the same engine, key registry, and security model.

Two APIs, One Module

The Fluent Builder — jwtNew()

When readability matters, the fluent builder gives you a clean, chainable surface for token construction. Call jwtNew() and chain your claims. Terminate with .sign() or .encrypt().

token = jwtNew()
    .subject( "user-123" )
    .issuer( "auth-service" )
    .audience( "mobile-client" )
    .claim( "roles", [ "admin", "user" ] )
    .expireIn( 3600 )
    .header( "kid", "v1" )
    .sign( secret, "HS256" );

Every standard claim has a named method. Custom claims go through .claim( key, val ). Headers via .header( key, val ). Swap .sign() for .encrypt() and you have a JWE. It reads like what it does. 🎯

The BIF Functions

For teams that prefer a direct, functional style, all operations are available as first-class BoxLang BIFs:

BIF Purpose
jwtCreate() Sign a payload struct into a compact JWS token
jwtVerify() Verify signature and validate claims — throws on failure
jwtValidate() Like jwtVerify() but returns true/false
jwtDecode() Inspect header/payload without signature verification
jwtRefresh() Re-issue a token with fresh iat, jti, and optional new exp
jwtEncrypt() Encrypt a payload as a compact JWE token
jwtDecrypt() Decrypt a JWE token and return claims
jwtGenerateSecret() Cryptographically random HMAC secret (Base64-encoded)
jwtGenerateKeyPair() RSA or EC key pair as PEM strings

Get Started in Seconds

HMAC Sign and Verify

secret  = jwtGenerateSecret( 256 );
token   = jwtCreate( { sub: "user-123", iss: "my-api", roles: [ "admin" ] }, secret, "HS256" );
payload = jwtVerify( token, secret, "HS256" );
writeOutput( payload.sub ); // user-123

RSA Sign and Verify

keys    = jwtGenerateKeyPair( "RS256" );
token   = jwtCreate( { sub: "user-123" }, keys.privateKey, "RS256" );
payload = jwtVerify( token, keys.publicKey, "RS256" );

JWE Encryption

Sensitive payloads — PII, PHI, internal claims that must stay opaque — belong in a JWE, not a JWS. bx-jwt handles both:

token   = jwtEncrypt(
    { sub: "patient-456", phi: { dob: "1990-01-15" } },
    secret32bytes,
    { keyAlgorithm: "dir", encAlgorithm: "A256GCM" }
);
payload = jwtDecrypt( token, secret32bytes, { keyAlgorithm: "dir", encAlgorithm: "A256GCM" } );

Or nest them — sign first, encrypt the signed token — for the full sign-then-encrypt pattern:

// Inner signed JWT
signedToken = jwtCreate( { sub: "u1", role: "admin" }, innerPrivKey, "RS256", {
    headers: { cty: "JWT" }
} );

// Outer encrypted JWE
encryptedToken = jwtEncrypt( signedToken, outerPubKey, {
    keyAlgorithm : "RSA-OAEP-256",
    encAlgorithm : "A256GCM"
} );

Enterprise Key Management with the Key Registry

This is where bx-jwt separates from basic JWT libraries. The Key Registry lets you define named keys once in configuration and reference them by name throughout your entire application. Keys never appear in application logic. Rotation is a config change, not a code change.

// ModuleConfig.bx
settings = {
    keys: {
        "api-signing": {
            algorithm : "HS256",
            secret    : "${Setting: env.JWT_HMAC_SECRET not found}"   // env var substitution built-in
        },
        "api-rsa": {
            algorithm  : "RS256",
            privateKey : "/etc/keys/api-private.pem",
            publicKey  : "/etc/keys/api-public.pem"
        },
        "partner-public": {
            algorithm : "RS256",
            publicKey : "/etc/keys/partner-public.pem"  // verify-only key
        }
    },
    defaultSigningKey : "api-signing",
    defaultVerifyKey  : "api-signing",
    defaultAlgorithm  : "HS256",
    defaultIssuer     : "my-api",
    defaultAudience   : "web",
    defaultExpiration : 3600,
    generateIat       : true,
    generateJti       : true
}

With defaults fully configured, the key and algorithm arguments become optional everywhere:

// No key argument, no algorithm argument — resolved from registry
token   = jwtCreate( { sub: "user-123" } );
payload = jwtVerify( token );

Keys can also be registered at runtime via the JWTService:

jwtService = getBoxContext().getRuntime().getGlobalService( "JWTService" );
jwtService.registerKey( "session-key", { algorithm: "HS256", secret: generateSecureKey() } );

Security by Default — Not by Configuration 🛡️

bx-jwt is built with the attack surface in mind. Security properties are unconditional — they cannot be turned off:

alg:none Rejection

The classic JWT attack. bx-jwt unconditionally rejects tokens with alg:none. Passing an unsigned token to jwtVerify() or jwtRefresh() always throws JWTVerificationException. No configuration switch, no override. It simply doesn't work.

HMAC Minimum Key Lengths (RFC 7518 §3.2)

Short HMAC secrets are a real-world vulnerability. bx-jwt enforces RFC 7518 minimums:

Algorithm Minimum Key Length
HS256 32 bytes (256 bits)
HS384 48 bytes (384 bits)
HS512 64 bytes (512 bits)

Use jwtGenerateSecret( bits ) and you're always compliant.

Algorithm Allowlist

Algorithm-confusion attacks exploit servers that accept any algorithm the token header declares. Lock your application to a known set:

// Only HS256 and RS256 are accepted — anything else throws
allowedAlgorithms: [ "HS256", "RS256" ]

Clock Skew Tolerance

Distributed systems have clock drift. bx-jwt ships with a configurable clockSkew (default: 60 seconds) that prevents legitimate tokens from failing exp/nbf validation due to minor time differences between services. Tune it per environment:

// Strict environment
payload = jwtVerify( token, secret, "HS256", { clockSkew: 0 } );

// Distributed system with known drift
payload = jwtVerify( token, secret, "HS256", { clockSkew: 120 } );

Real-World Patterns

Authentication Middleware

function requireAuth() {
    var authHeader = getHttpRequestData().headers[ "Authorization" ] ?: ""
    if ( !authHeader.startsWith( "Bearer " ) ) {
        bx:header statusCode=401;
        abort;
    }

    var token = authHeader.removeFirst( "Bearer " )

    if ( !jwtValidate( token, application.jwtSecret, "HS256" ) ) {
        bx:header statusCode=401;
        abort;
    }

    request.currentUser = jwtVerify( token, application.jwtSecret, "HS256", {
        claims: { iss: "auth-service", aud: "api" }
    } );
}

Token Refresh with Grace Period

function refreshToken( token ) {
    try {
        return jwtRefresh( token, application.jwtSecret, "HS256", {
            allowExpired : true,   // honor recently expired tokens
            expireIn     : 3600,
            claims       : { iss: "auth-service" }
        } );
    } catch ( "bxjwt.JWTVerificationException" e ) {
        // Bad signature — not refreshable
        return "";
    }
}

Kid-Based Key Rotation

function verifyWithKeyRotation( token ) {
    var decoded = jwtDecode( token );
    var kid     = decoded.header.kid ?: "default";
    var key     = getKeyForKid( kid );
    return jwtVerify( token, key, decoded.header.alg );
}

Full Algorithm Support

Signing (JWS)

Algorithm Type Notes
HS256, HS384, HS512 HMAC Symmetric
RS256, RS384, RS512 RSA Asymmetric — private signs, public verifies
ES256, ES384, ES512 Elliptic Curve Smaller keys than RSA, equivalent security

Encryption (JWE)

Key Algorithm Content Encryption Key Type
RSA-OAEP-256 A256GCM RSA key pair
dir A256GCM 256-bit symmetric secret

Installation

# CommandBox
box install bx-jwt

# BoxLang CLI
install-bx-module bx-jwt

bx-jwt requires a BoxLang+ or BoxLang++ subscription. 🔑

This module ships as part of our enterprise module collection — a growing library of production-ready, security-focused, professionally maintained modules available exclusively to BoxLang+ subscribers.

BoxLang+/++/Starter

bx-jwt is one of many enterprise modules available under BoxLang+/++/Starter. When you subscribe, you get:

  • 🔐 bx-jwt and the full enterprise module library
  • ⚡ Priority support from the Ortus team
  • 🏗️ Access to upcoming enterprise modules as they ship
  • ❤️ You fund the continued development of BoxLang as a community-supported open source project
    View Plans & Subscribe → boxlang.io/plans

Resources

JSON Web Tokens are a solved problem. Now BoxLang has the enterprise solution to prove it. Install bx-jwt, protect your applications, and ship with confidence. 🚀

The post Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang appeared first on foojay.

]]>
https://foojay.io/today/introducing-bx-jwt-enterprise-grade-json-web-tokens-for-boxlang/feed/ 1
Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/ https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/#respond Thu, 21 May 2026 11:42:26 +0000 https://foojay.io/?p=123899 Table of Contents 🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste🎓 What Is a Skill?📥 Install in Seconds: Two Paths, One Standard ⚡ Option 1 — npx skills (works everywhere) 🥊 Option 2 — ColdBox CLI (deep BoxLang/ColdBox integration) ...

The post Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World appeared first on foojay.

]]>

Table of Contents
🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste🎓 What Is a Skill?📥 Install in Seconds: Two Paths, One Standard

🔷 Core Repositories — Curated by Ortus⭐ A Taste of What's Available🌐 Submit Your Own — Community Skills, Security First🛠 How Your Agent Actually Uses It🔮 Why This Matters Beyond BoxLang🎯 Get Started Now📚 Resources


Today we're launching something we've been quietly building for months: skills.boxlang.io — a public, agent-agnostic directory for AI skills covering BoxLang, ColdBox, TestBox, CommandBox, and the entire Ortus ecosystem.

If you've ever pasted a 400-line system prompt into yet another AI agent, watched two of your bots drift onto subtly different versions of the same coding standard, or spent half a Friday afternoon trying to convince an LLM that BoxLang is not Java and is not CFML, or how to code for Modern CFML; this launch is for you. 🎯

The numbers at launch:

  • 203+ curated skills available on day one
  • 8,000+ installs already, before public announcement
  • 3 core repositories maintained directly by Ortus Solutions
  • Multiple agents supported — Claude Code, Cursor, GitHub Copilot, Codex, OpenCode, and more
    Let's dig into what it is, why we built it, and how to start using it in the next 30 seconds. 🚀

🤔 The Problem: AI Knowledge Doesn't Scale by Copy-Paste

Every team building with AI agents eventually hits the same wall.

You write a great system prompt that teaches an agent your SQL conventions. Then a teammate spins up a new bot and pastes a slightly older version. A month later there's a third variant in a Slack snippet that nobody can find. Your "single source of truth" is now three sources of conflict, and the agent's outputs reflect every one of them.

This isn't a discipline problem — it's an architecture problem. System prompts are plain strings, and plain strings don't have a source of truth. They aren't versioned, aren't audited, aren't shared, and aren't discoverable.

Anthropic's Agent Skills open standard — Markdown files with frontmatter metadata, distributed as SKILL.md — gave the industry a real answer. BoxLang AI 3.0 implemented it natively. And now skills.boxlang.io brings the missing piece: a public, curated, security-audited registry where these skills live, are versioned, and can be installed into any AI agent in seconds. 💚

🎓 What Is a Skill?

A skill is a portable, reusable unit of expertise — a SQL coding style guide, a tone-of-voice policy, a ColdBox conventions cheat sheet, an API design standard, a security ruleset. Anything your AI assistant should know before it starts answering.

Each skill is a Markdown file (SKILL.md) with optional YAML frontmatter:

---
description: Use this skill when writing, reviewing, or formatting any
  Ortus Solutions code (BoxLang, CFML, or Java) to ensure it follows
  the official Ortus coding standards.
tags: [boxlang, cfml, java, coding-standards, ortus]
---

# Ortus Coding Standards

Always use spacing inside parentheses and brackets for readability.
Prefer closures with `=>` over anonymous functions.
Use lambdas with `->` when no external scope is needed.
...

Define it once. Inject it everywhere. Let your codebase — not your clipboard — be the source of truth. 📚

📥 Install in Seconds: Two Paths, One Standard

We built skills.boxlang.io to be agent-agnostic. Whatever AI tool your team prefers, the skills work the same way. You have two install paths.

⚡ Option 1 — npx skills (works everywhere)

Powered by skills.sh, an open-source, agent-agnostic CLI for discovering, installing, and managing SKILL.md files across Claude Code, GitHub Copilot, Cursor, Codex, and more. It reads the BoxLang Skills Hub catalog, security-audits community content, and drops files into the correct agent directory in one command.

# Install an entire repository of skills
npx skills add ortus-boxlang/skills

# Or grab a single, focused skill
npx skills add ortus-boxlang/skills/coldbox-basics

No global install needed. Works with any Node.js. 🌐

🥊 Option 2 — ColdBox CLI (deep BoxLang/ColdBox integration)

If you're already living in the ColdBox world, the ColdBox CLI 8.11 release wires the directory directly into your project workflow:

# Browse the directory interactively
coldbox ai skills install --list

# Filter by source or category
coldbox ai skills install --list coldbox/skills
coldbox ai skills install --list coldbox/skills/coldbox-testing

# Install a specific skill
coldbox ai skills install ortus-boxlang/skills/async-programming

# Search the registry
coldbox ai skills find "rest api"

Bonus: when you box install a module that has skills published to the directory, coldbox ai refresh auto-installs them. Skills become infrastructure, not setup. 💚

🔷 Core Repositories — Curated by Ortus

Three core repositories are officially maintained by Ortus Solutions. Skills here are trusted by default and skip the community audit step.

Repository Focus
ortus-boxlang/skills BoxLang language, runtime, BIFs, and core modules
coldbox/skills ColdBox MVC framework patterns and conventions
ortus-solutions/skills WireBox, TestBox, LogBox, and the broader Ortus module library

Want a skill added to a core repo? Open a pull request. Add your SKILL.md inside a new folder, include valid YAML frontmatter, and the Ortus team will review and merge it. Once merged, it's automatically imported the next time the hub syncs. ⚡

⭐ A Taste of What's Available

A small sample of skills you'll find in the directory at launch:

  • code-documenter — Producing or improving developer-facing documentation for codebases, APIs, modules, and architecture decisions
  • ortus-java-coding-standards — Official Ortus formatting and structural conventions for BoxLang, CFML, and Java
  • javascript-expert — Modern JavaScript correctness, async flows, module design, and architectural refactors
  • alpinejs-expert — Alpine.js component state, directives, transitions, and reusable stores
  • vite-expert — Vite-based frontend builds, HMR diagnostics, plugin customization, and Vitest integration
  • vuejs-expert — Composition API patterns, routing, forms, testing, and SSR-aware component design
  • async-programming — BoxLang futures, parallel execution, and concurrency primitives
  • coldbox-basics — ColdBox MVC conventions, handlers, models, interceptors, and module architecture
    …and 195+ more. Browse the full directory at skills.boxlang.io/skills. 🎯

🌐 Submit Your Own — Community Skills, Security First

Don't want to contribute to a core repo? Publish your own GitHub repository as a Community source or send us a Pull Request to any of our repos. Community skills are listed alongside core skills in the directory and go through automated security auditing before being made available, so consumers can install them with confidence.

The submission flow is straightforward:

  • Create a GitHub repository with one or more SKILL.md files, each in its own subfolder (e.g. my-skill/SKILL.md)
  • Add YAML frontmatter with at minimum name, description, and tags
  • Write clear, accurate documentation in the Markdown body
  • Submit your repo and we'll review it
    You keep full ownership and control of your skills. The hub just makes them discoverable and installable. 💚

🛠 How Your Agent Actually Uses It

After installing, skills land in ~/.ai/skills/, ~/.claude/skills/, or the equivalent directory for your agent. Your AI assistant automatically discovers and loads them in each conversation.

The change in agent behavior is immediate. Ask things like:

  • "Write a ColdBox REST handler with full error handling"
  • "Create a WireBox-managed singleton service that queries SQLite"
  • "Show me how to use TestBox to write integration tests"
  • "Help me configure bx-migrations for my BoxLang app"

…and the agent answers using patterns and idioms from the installed skills, not scattered (and often outdated) snippets pulled from random internet training data. The hallucinations go down. The accuracy goes up. The output starts to feel like it was written by someone who actually knows the framework — because, in a sense, it now was. 🎓

🔮 Why This Matters Beyond BoxLang

We didn't build skills.boxlang.io as a marketing site. We built it because the Ortus ecosystem — BoxLang, ColdBox, TestBox, CommandBox, WireBox, LogBox, CacheBox, hundreds of modules across 18+ years of work — is too rich to fit into anyone's training data, and too valuable to be re-discovered through trial and error every time a developer opens a new chat with their AI assistant.

A public, curated, audited skills directory means:

  • Module authors can ship AI knowledge alongside their code
  • Teams can standardize agent behavior across every developer's workstation
  • Newcomers get accurate, idiomatic guidance from day one
  • The community owns and contributes to a shared knowledge layer that compounds over time

This is the same shift package managers brought to language ecosystems — except for AI knowledge. It's the era of skills, and now every BoxLang and ColdBox developer can participate. 🚀

🎯 Get Started Now

# Install your first skill in 10 seconds
npx skills add ortus-boxlang/skills

# Or via the ColdBox CLI
coldbox ai skills install --list

Then point your AI agent at your codebase and watch the difference. ⚡

📚 Resources

Got a skill you'd love to publish, or one you wish existed? We'd love to hear from you — open a PR, submit your repo, or drop us a note. The directory grows because the community grows. 💚

The post Introducing skills.boxlang.io — The Open Agent Skills Ecosystem for BoxLang & the Ortus World appeared first on foojay.

]]>
https://foojay.io/today/introducing-skills-boxlang-io-the-open-agent-skills-ecosystem-for-boxlang-the-ortus-world/feed/ 0
BoxLang v1.13.0: Compatibility, Concurrency, and Formatter Maturity https://foojay.io/today/boxlang-v1-13-0-compatibility-concurrency-and-formatter-maturity/ https://foojay.io/today/boxlang-v1-13-0-compatibility-concurrency-and-formatter-maturity/#respond Tue, 19 May 2026 12:11:19 +0000 https://foojay.io/?p=123809 Table of Contents New Features Character-Aware Trimming — trim(), ltrim(), rtrim() getClassMetadata() by Absolute Path SystemExecute() Environment Controls The BoxLang Formatter Goes Production-ReadyAsync & Concurrency HardeningMiniServer: Security & ReliabilityCompatibility WinsChangelog Highlights BoxLang 1.13.0 is a stability-first release with deep compatibility ...

The post BoxLang v1.13.0: Compatibility, Concurrency, and Formatter Maturity appeared first on foojay.

]]>

Table of Contents
New Features

The BoxLang Formatter Goes Production-ReadyAsync & Concurrency HardeningMiniServer: Security & ReliabilityCompatibility WinsChangelog Highlights


BoxLang 1.13.0 is a stability-first release with deep compatibility work and runtime hardening. This build closes 48 issues, with the majority focused on CFML compatibility edge cases, concurrency correctness, formatting parity, and miniserver/runtime reliability under real production loads.

While this release is bug-fix heavy, it still introduces several meaningful features and quality-of-life improvements: character-aware trimming, class metadata lookup by absolute path, process environment control in SystemExecute(), SOAP headers, new query column rename capabilities, and safer miniserver routing/security defaults.

New Features

Three additions that materially expand what the runtime can do.

Character-Aware Trimming — trim(), ltrim(), rtrim()

The string trimming BIFs now accept an optional chars argument. Strip arbitrary character sets without reaching for rereplace().

"**Urgent**".trim( "*" )       // "Urgent"
"000123".ltrim( "0" )          // "123"
"report....".rtrim( "." )      // "report"
"//path/to/dir//".trim( "/" )  // "path/to/dir"

Each character in chars is treated as an independent trim target — the same behavior you'd expect from Python or JavaScript. One less regex workaround.

getClassMetadata() by Absolute Path

Class metadata can now be loaded directly from a filesystem path, bypassing the class loader and import resolution entirely.

meta = getClassMetadata( "/opt/apps/models/User.bx" )
writeDump( meta.name )        // "User"
writeDump( meta.properties )  // array of property definitions
writeDump( meta.functions )   // array of function signatures

This is a cornerstone API for tooling. Linters, IDE integrations, documentation generators, and migration scanners can now inspect .bx and .cfc files without booting them into the runtime, firing onApplicationStart, or wrestling with import edge cases. The kind of unglamorous primitive that makes an ecosystem possible.

SystemExecute() Environment Controls

Two new arguments give you deterministic control over the environment of spawned child processes:

  • inheritEnvironment (boolean, default true) — when false, the child starts with a clean slate
  • environment (struct) — an explicit map of variables to inject
    result = systemExecute(
        name               = "env",
        arguments          = "",
        inheritEnvironment = false,
        environment        = {
            APP_ENV   : "production",
            DB_HOST   : "internal.db.example.com",
            FEATURE_X : "true"
        }
    )
    
    writeOutput( result.output )
    

Before 1.13.0, every systemExecute() call inherited the full parent environment — including secrets, tokens, and internal config. Security-conscious deployments now have an explicit, auditable way to lock that down.

The BoxLang Formatter Goes Production-Ready

This is a flagship moment. The formatter graduates from experimental to production-grade and lands with a complete CI/CD integration surface.

The outcome you actually care about: when formatting is enforced in CI, pull requests stop being about whitespace and start being about logic again. For mixed BoxLang/CFML codebases, the legacy .cfformat.json compatibility path means you can adopt the formatter on legacy code today and migrate to BoxLang-native defaults on your own timeline.

Capabilities:

  • In-place formattingboxlang format --input ./ formats an entire project tree
  • CI check modeboxlang format --check --input ./ exits non-zero on any unformatted file (drop straight into GitHub Actions, GitLab CI, or Jenkins)
  • Stdout modeboxlang format --overwrite false --input ./models/User.cfc for diff-friendly previews
  • Multi-extension.bx, .bxs, .bxm, .cfm, .cfc, .cfs in a single pass

Config discovery fallback chain:

  • .bxformat.json — BoxLang-native config (Ortus gold-standard defaults)
  • .cfformat.json — legacy CFFormat config, auto-converted with migration-safe defaults
  • Built-in defaults — sensible behavior with zero config

Migration tooling built in:

# Generate a fresh .bxformat.json with defaults
boxlang format --initConfig

# Convert an existing .cfformat.json to .bxformat.json
boxlang format --convertConfig --input ./

Async & Concurrency Hardening

Concurrency bugs are the worst kind of bug — intermittent, non-deterministic, catastrophic when they hit production. 1.13.0 closes several long-standing race conditions and lifecycle issues across the async subsystem and threading layer.

API surface normalization. Missing async methods are restored: all(), allApply(), thenAsync(), delay(), and shutdownAndAwaitTermination() now exist with correct signatures. Positional spread arguments (...args) are supported in calls — unblocking a common functional-programming pattern.

args     = [ "Ada", "Lovelace" ]
fullName = formatName( ...args )

BoxFuture() lifecycle. A BoxFuture created during an HTTP request used to throw scope-access errors if the parent request completed before the future resolved. The context lifecycle is now properly decoupled — background work survives request teardown without touching stale scopes.

Concurrent array iteration. for/in loops over arrays no longer throw ConcurrentModificationException when the array is mutated from another thread.

Atomic class file writes. Class generation now uses a temp-file-then-atomic-rename pattern. No more transient zero-byte .class artifacts surfacing under parallel compilation — a race condition that produced some genuinely painful ClassNotFoundException reports in production.

MiniServer: Security & Reliability

The headline: a misconfigured miniserver no longer accidentally serves your source code or configuration over HTTP. The static-serving security filter now blocks hidden files and dotfiles, framework config artifacts (.boxlang.json, boxlang.json), and source files (.bx, .cfc) when not routed through the engine.

Pass predicate is now configurable through three channels — pick whichever fits your deployment model:

# CLI
boxlang server start --pass-predicate "/api/*"
// boxlang.json
{
  "web": {
    "passPredicate": "/api/*"
  }
}
# Environment variable
export BOXLANG_PASS_PREDICATE="/api/*"

Transfer reliability fixes:

  • Chunked encoding truncation fixed for large file responses (above the default buffer size)
  • Empty text-file uploads no longer throw illegal-state errors
  • content-length headers correctly computed across all response paths

Compatibility Wins

CFML compatibility is a continuous workstream, not a one-time port. This release closes a handful of high-impact gaps that real applications were tripping over.

SOAP header support. Consumers can now include optional <Header> blocks for WS-Security, transactional metadata, and routing.

soapService.call(
    method  = "processOrder",
    headers = { Security : { UsernameToken : { Username : "admin" } } }
)

query.setColumnNames(). Query objects now support column renaming through a dedicated method, matching the Adobe CF and Lucee API.

q = queryNew( "fname,lname", "varchar,varchar", [ [ "Ada", "Lovelace" ] ] )
q.setColumnNames( [ "firstName", "lastName" ] )
writeDump( q.columnList )  // "firstName,lastName"

CLI .box.env support. The CLI now reads ~/.box.env on startup, loading user-level environment variables that persist across sessions.

# ~/.box.env
DB_HOST=localhost
DB_PORT=5432

Runtime Hardening
The unsexy stuff that matters. A condensed view of the deeper fixes shipped in this release:

Area What Changed
Abort semantics Corrected in web runtime Java try/catch boundaries
AppCDS paths Deterministic, per-binary paths on Windows
Superclass init Failed init no longer blocks class recreation retries
Module onLoad() Request-context setup fixed for dump() template behavior
REST CFC mapping Service-name routing corrected
Class creation Broad performance optimizations in class loading and locator
JSA packages Path handling fixed for BOXLANG_HOME with spaces
Zero timespan createTimeSpan( 0, 0, 0, 0 ) now correctly interpreted as no-cache
Remote methods Force-write correctly under enableOutputOnly
Binary writes Valid downloaded ZIP output restored
Numeric parsing Leading-zero strings parsed safely
QoQ nesting Nested-parentheses predicate parsing corrected
Custom tags this scope no longer leaks from custom-tag context
numberFormat() Major mask compatibility sweep across multiple tickets

Changelog Highlights

New Features

BL-2348: trim(), ltrim(), rtrim() gain chars argument
BL-2349: getClassMetadata() accepts absolute filesystem path
BL-2390: SystemExecute() gains inheritEnvironment and environment arguments

Improvements

BL-2078: SOAP header support for auth and security blocks
BL-2333: query.setColumnNames() compatibility API
BL-2354: Miniserver pass predicate configurability (CLI, JSON, env var)
BL-2355: Miniserver security handler upgrades
BL-2378: CLI reads ~/.box.env on startup
BL-2393: Chunked encoding truncation fix for large file responses
BL-2398: BoxLang-native formatting defaults aligned with Ortus conventions

Notable Bug Fixes

BL-2269: Missing async methods and signatures restored
BL-2336: Abort semantics corrected in web runtime try/catch boundaries
BL-2360: Positional spread arguments supported in calls
BL-2372: Concurrent modification exception fixed for array for/in
BL-2373: Class-file write race fixed with atomic write pattern
BL-2376: BoxFuture() context lifecycle fix after HTTP request completion
BL-2382: Binary write path fixed for valid downloaded ZIP output
BL-2386: QoQ nested-parentheses predicate parsing corrected
BL-2394: Custom-tag context no longer leaks incorrect this scope

View the full release report

BoxLang 1.13.0 is available now. Head to boxlang.io to get started, dig into the docs, and join us on the Ortus Community Slack to share what you're building.

The post BoxLang v1.13.0: Compatibility, Concurrency, and Formatter Maturity appeared first on foojay.

]]>
https://foojay.io/today/boxlang-v1-13-0-compatibility-concurrency-and-formatter-maturity/feed/ 0
BoxLang AI Series: Complete Guide to Building AI Agents https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/ https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/#respond Thu, 14 May 2026 09:26:48 +0000 https://foojay.io/?p=123758 Table of Contents Start Here: A Practical OverviewThe Full SeriesWhat You’ll LearnKey ResourcesWhy BoxLang AIReady to Start Building? The world of AI development is moving fast, but building real, production-ready AI agents doesn’t have to be complex. This series walks ...

The post BoxLang AI Series: Complete Guide to Building AI Agents appeared first on foojay.

]]>

Table of Contents
Start Here: A Practical OverviewThe Full SeriesWhat You’ll LearnKey ResourcesWhy BoxLang AIReady to Start Building?


The world of AI development is moving fast, but building real, production-ready AI agents doesn’t have to be complex.

This series walks you step by step through how to design, build, and deploy AI agents using BoxLang AI. Whether you’re exploring AI for the first time or looking to modernize your current applications, these guides will help you move from concept to implementation with clarity.

Start Here: A Practical Overview

If you’re new to BoxLang AI or want to understand what’s possible before diving into the technical details, start here:

https://foojay.io/today/how-to-develop-ai-agents-using-boxlang-ai-a-practical-guide/

This guide provides a high-level view of how to build AI agents, integrate multiple models, and design real-world workflows using BoxLang.

The Full Series

Follow the series in order to go from fundamentals to advanced implementations:

What You’ll Learn

Across this series, you’ll learn how to:

  • Build AI agents with memory, tools, and reasoning capabilities
  • Connect to multiple AI providers with a single unified API
  • Implement Retrieval-Augmented Generation (RAG) pipelines
  • Work with vector databases and document ingestion
  • Design scalable, production-ready AI workflows
  • Deploy AI agents in modern cloud environments

Key Resources

To help you go deeper and start building right away:

Why BoxLang AI

BoxLang AI is designed to remove the complexity of working with multiple AI providers and tools. With a single API, you can build powerful AI-driven applications without vendor lock-in, while maintaining full control over your architecture.

If you’re working with legacy systems, BoxLang also allows you to introduce AI capabilities incrementally without needing a full rewrite.

Ready to Start Building?

Explore the series, try the examples, and start building your own AI agents today.

If you have questions or want to see how this can apply to your existing systems, feel free to reach out to the Ortus team.

Next →

The post BoxLang AI Series: Complete Guide to Building AI Agents appeared first on foojay.

]]>
https://foojay.io/today/boxlang-ai-series-complete-guide-to-building-ai-agents/feed/ 0
How to Develop AI Agents Using BoxLang AI: A Practical Guide https://foojay.io/today/how-to-develop-ai-agents-using-boxlang-ai-a-practical-guide/ https://foojay.io/today/how-to-develop-ai-agents-using-boxlang-ai-a-practical-guide/#respond Tue, 12 May 2026 12:52:39 +0000 https://foojay.io/?p=123748 Table of Contents What we'll CoverPrerequisites Step 1 — Install BoxLang Step 2 — Install the bx-ai Module Step 3 — Set Up Your .env File Step 4 — Configure config/boxlang.json Step 5 — Run Your First Script What Are ...

The post How to Develop AI Agents Using BoxLang AI: A Practical Guide appeared first on foojay.

]]>

Table of Contents
What we'll CoverPrerequisites

What Are AI Agents?What Is BoxLang AI?Core Concept 1: ToolsCore Concept 2: MemoryCore Concept 3: The AgentHow to Put It All Together

Streaming Responses

How the Agent ThinksGoing Further

ConclusionResources


AI agents are transforming how we build software. Unlike traditional chatbots that just answer questions, agents can reason about what tools they need, decide when to use them, chain multiple actions together, and remember what happened earlier in a conversation.

In this tutorial, I'll show you how to build a real-world AI agent using BoxLang AI — the official AI framework for the BoxLang JVM language. We'll build SupportBot, an e-commerce customer support agent that can look up orders, check inventory, issue refunds, and answer questions grounded in your knowledge base.

By the end you'll understand how AI agents work under the hood, and you'll have a fully working agent you can adapt for your own domain.

What we'll Cover

  • Prerequisites
  • What Are AI Agents?
  • What Is BoxLang AI?
  • Core Concept 1: Tools
  • Core Concept 2: Memory
  • Core Concept 3: The Agent
  • How to Put It All Together
  • Streaming Responses
  • How the Agent Thinks
  • Going Further
  • Conclusion

Prerequisites

Before diving in, you should be comfortable with:

BoxLang basics — You should know how to write BoxLang scripts, work with structs and arrays, and understand closures. If you're new, start with the Quick Start Guide.

Basic LLM familiarity — Knowing what a large language model is and having used one (via aiChat() or similar) will help you follow along.

Step 1 — Install BoxLang

Download and install BoxLang from boxlang.io, or use BVM (BoxLang Version Manager) to manage multiple versions:

# Install BVM
/bin/bash -c "$(curl -fsSL https://downloads.ortussolutions.com/ortussolutions/bvm/install.sh)"

# Install the latest BoxLang
bvm install latest
bvm use latest

# Verify
boxlang --version

Step 2 — Install the bx-ai Module

Install bx-ai locally into your project using the built-in module installer:

# Creates a boxlang_modules/ folder in your project
install-bx-module bx-ai --local

Your project structure will look like this:

my-project/
├── boxlang_modules/
│   └── bxai/               ← installed here
├── config/
│   └── boxlang.json        ← BoxLang configuration
├── .env                    ← your API keys (never commit this)
├── .env.example            ← template to share with your team
├── .gitignore
└── agent.bxs               ← your BoxLang scripts

Step 3 — Set Up Your .env File

Copy .env.example to .env and fill in at least one provider API key. Never commit .env to source control.

.env.example — commit this template so your team knows what keys are needed:

# BoxLang Custom Configuration — points BoxLang at your config file
BOXLANG_CONFIG=./config/boxlang.json

# AI Provider API Keys — fill in at least one
OPENAI_API_KEY=your-api-key
CLAUDE_API_KEY=your-api-key
GEMINI_API_KEY=your-api-key
GROK_API_KEY=your-api-key
GROQ_API_KEY=your-api-key
PERPLEXITY_API_KEY=your-api-key
OPENROUTER_API_KEY=your-api-key
MISTRAL_API_KEY=your-api-key
HUGGINGFACE_API_KEY=your-api-key
VOYAGE_API_KEY=your-api-key
COHERE_API_KEY=your-api-key
# AWS Bedrock
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_REGION=us-east-1

.env — your actual keys, never committed:

BOXLANG_CONFIG=./config/boxlang.json
OPENAI_API_KEY=sk-proj-...

Add .env to your .gitignore:

.env
boxlang_modules/

Step 4 — Configure config/boxlang.json

BoxLang reads its configuration from the file pointed to by BOXLANG_CONFIG. The ${Setting: VAR_NAME not found} syntax reads directly from your .env file — your keys never live in the config file itself.

config/boxlang.json:

{
    "modules": {
        "bxai": {
            "settings": {
                "provider": "openai",
                "apiKey": "${Setting: OPENAI_API_KEY not found}",
                "defaultParams": {
                    "model": "gpt-4o",
                    "temperature": 0.2
                }
            }
        }
    }
}

Step 5 — Run Your First Script

Create agent.bxs and run it:

// agent.bxs
answer = aiChat( "What is BoxLang AI in one sentence?" )
println( answer )
boxlang agent.bxs

That's it — no build step, no compile, no server. BoxLang reads .env automatically, loads the bxai module from boxlang_modules/, and runs.

Switching Providers

To switch from OpenAI to Claude, change two lines in config/boxlang.json and add the key to .env:

{
    "modules": {
        "bxai": {
            "settings": {
                "provider": "claude",
                "apiKey": "${Setting: CLAUDE_API_KEY not found}",
                "defaultParams": {
                    "model": "claude-sonnet-4-5-20251001"
                }
            }
        }
    }
}

Your agent.bxs code doesn't change at all. This is the zero-vendor-lock-in promise in practice.

"💡 bx-ai supports 17 providers — OpenAI, Claude, Gemini, Ollama, Groq, and more. You can also run fully local AI with Ollama — no API key required, zero cost, complete privacy. See the provider docs for per-provider configuration."

What Are AI Agents?

Think of an AI agent as a chatbot that can act, not just respond. A traditional chatbot answers questions from what it knows. An agent can reach out and do things — query databases, call APIs, read files, send emails — and chain those actions together to solve multi-step problems.

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   TRADITIONAL CHATBOT           AI AGENT                    │
│   ──────────────────            ────────                    │
│                                                             │
│   User ──► LLM ──► Answer       User ──► Agent              │
│                                           │                 │
│   One shot. No tools.                     ├──► Tool A       │
│   No memory.                              ├──► Tool B       │
│                                           ├──► Memory       │
│                                           └──► Answer       │
│                                                             │
│                                 Reasons. Acts. Remembers.   │
└─────────────────────────────────────────────────────────────┘

Here's a conversation with the SupportBot we'll build:

User:  "Where is order #ORD-78291? It was supposed to arrive yesterday."

Agent: [Thinks: I need to look up that order]
Agent: [Calls get_order( orderId: "ORD-78291" )]
Agent: [Gets back: { status: "In Transit", carrier: "FedEx",
                     tracking: "794644792798",
                     estimatedDelivery: "2026-04-04" }]

Agent: "Your order #ORD-78291 is in transit with FedEx
        (tracking: 794644792798). It was delayed by one day
        and is now estimated to arrive tomorrow, April 4th."

The agent broke the problem down, picked the right tool, and synthesized the answer. This matters when:

  • Queries don't fit into predefined categories
  • Answering requires combining data from multiple sources
  • Users need to follow up on previous answers

What Is BoxLang AI?

BoxLang AI (bx-ai) is the official AI framework for BoxLang — a modern, dynamic JVM language. It provides a unified, fluent API for building AI agents, multi-model workflows, RAG pipelines, and AI-powered applications.

┌────────────────────────────────────────────────────────────────┐
│                     BoxLang AI Stack                           │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│   Your Application Code                                        │
│   ─────────────────────────────────────────────────────────    │
│   aiAgent()  aiChat()  aiEmbed()  aiMemory()  aiTool()         │
│                                                                │
│   ─────────────────────────────────────────────────────────    │
│   Skills │ Middleware │ Tool Registry │ Memory │ Pipelines     │
│                                                                │
│   ─────────────────────────────────────────────────────────    │
│   OpenAI │ Claude │ Gemini │ Ollama │ Groq │ + 12 more         │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Key properties that make it great for building agents:

- One API, 17 providers — switch from OpenAI to Claude by changing a config value, not code
- aiAgent() BIF — a fully featured agent with tools, memory, skills, and middleware
- Fluent tool definition — turn any closure into an AI-callable tool with aiTool()
- Multi-tenant memory — one agent instance safely handles thousands of concurrent users
- JVM-native — runs everywhere Java runs, with full Java interop

Core Concept 1: Tools

Tools are functions your AI agent can call. The framework passes the tool's name, description, and parameter schema to the LLM, which decides when and how to call them. When the LLM decides to use a tool, BoxLang AI executes it and feeds the result back.

┌──────────────────────────────────────────────────────────────┐
│                    How Tools Work                            │
│                                                              │
│  ┌─────────┐    "I need order data"    ┌──────────────────┐  │
│  │   LLM   │ ─────────────────────── ► │  get_order()     │  │
│  │         │                           │  • name          │  │
│  │         │ ◄───────────────────────  │  • description   │  │
│  └─────────┘    { status, tracking }   │  • parameters    │  │
│                                        └──────────────────┘  │
│                                                              │
│  The LLM reads the description to decide WHEN to call.       │
│  BoxLang AI handles the execution and result passing.        │
└──────────────────────────────────────────────────────────────┘

Defining a Tool with aiTool()

The simplest way to create a tool is with the aiTool() BIF and a closure:

getWeatherTool = aiTool(
    "get_weather",
    "Get the current weather for a city. Use when the user asks about weather conditions.",
    ( required city ) => {
        // In a real app you'd call a weather API here
        return { temp: 72, condition: "sunny", city: arguments.city }
    }
)

The three arguments are: name, description, and callable. The description is what the LLM reads to decide whether this is the right tool — write it like you're telling a colleague when to use it.

A Real Tool: get_order

Here's the first tool for our SupportBot. It looks up an order by ID:

// OrderTools.bx
class {

    property name="orderService";

    function init( required any orderService ) {
        variables.orderService = arguments.orderService
        return this
    }

    @AITool( "Retrieve a single order by order ID. Use first when a customer mentions a specific order number. Always call this before attempting a refund or cancellation." )
    public struct function get_order( required string orderId ) {
        var order = variables.orderService.findById( arguments.orderId )

        if ( isNull( order ) ) {
            return {
                found   : false,
                orderId : arguments.orderId,
                message : "Order #arguments.orderId# was not found. Please verify the order ID."
            }
        }

        return {
            found            : true,
            orderId          : order.getId(),
            status           : order.getStatus(),
            carrier          : order.getCarrier(),
            trackingNumber   : order.getTrackingNumber(),
            estimatedDelivery: order.getEstimatedDelivery().dateFormat( "long" ),
            items            : order.getItems().map( item => {
                return { name: item.getName(), qty: item.getQty(), price: item.getPrice() }
            } ),
            total            : order.getTotal(),
            summary          : "Order ##arguments.orderId# — #order.getStatus()# — Est. delivery: #order.getEstimatedDelivery().dateFormat( 'long' )#"
        }
    }

}

A few things to notice:

The @AITool annotation tells the AIToolRegistry scanner that this method is an AI-callable tool. The annotation value becomes the tool's description. When you call aiToolRegistry().scan( new OrderTools( orderService ), "support" ), it registers get_order@support automatically.

The return value includes a summary field. Rather than making the LLM parse a raw struct, you pre-compute a one-sentence summary it can read directly. Return both the data (for detailed reasoning) and the summary (for quick reading).

The not-found case returns a helpful struct instead of throwing. The LLM sees found: false and the message and can relay that to the user clearly — far better than an unhandled exception.

The Full OrderTools Class

class {

    property name="orderService";

    function init( required any orderService ) {
        variables.orderService = arguments.orderService
        return this
    }

    @AITool( "Retrieve a single order by order ID. Use first when a customer mentions a specific order number." )
    public struct function get_order( required string orderId ) {
        var order = variables.orderService.findById( arguments.orderId )
        if ( isNull( order ) ) {
            return { found: false, message: "Order #arguments.orderId# not found." }
        }
        return {
            found            : true,
            orderId          : order.getId(),
            status           : order.getStatus(),
            carrier          : order.getCarrier(),
            trackingNumber   : order.getTrackingNumber(),
            estimatedDelivery: order.getEstimatedDelivery().dateFormat( "long" ),
            total            : order.getTotal(),
            summary          : "Order ##arguments.orderId# — #order.getStatus()#"
        }
    }

    @AITool( "Search a customer's order history. Use when the customer asks about past orders, spending history, or recent purchases." )
    public struct function search_orders(
        required string customerEmail,
        string  status = "",
        numeric limit  = 10
    ) {
        var orders = variables.orderService.findByEmail(
            email  : arguments.customerEmail,
            status : arguments.status,
            limit  : arguments.limit
        )
        return {
            count  : orders.len(),
            orders : orders.map( o => { id: o.getId(), status: o.getStatus(), total: o.getTotal(), date: o.getCreatedAt().dateFormat( "short" ) } ),
            summary: "Found #orders.len()# orders for #arguments.customerEmail#"
        }
    }

    @AITool( "Issue a refund for a specific order. IMPORTANT: Only call this after confirming the order exists and the customer has explicitly requested a refund." )
    public struct function issue_refund(
        required string orderId,
        required string reason
    ) {
        var result = variables.orderService.refund(
            orderId: arguments.orderId,
            reason : arguments.reason
        )
        return {
            success       : result.isSuccess(),
            refundId      : result.getRefundId(),
            amount        : result.getAmount(),
            processingDays: 5,
            summary       : result.isSuccess()
                ? "Refund of $#result.getAmount()# issued for order ##arguments.orderId#. Allow 5 business days."
                : "Refund failed: #result.getError()#"
        }
    }

}

Tool Design Principles

┌─────────────────────────────────────────────────────────────────┐
│                  The 4 Tool Design Rules                        │
│                                                                 │
│  1. DESCRIPTION ── Tell the LLM exactly when (and when NOT)     │
│                    to call this tool. Be specific.              │
│                                                                 │
│  2. SUMMARY     ── Always return a pre-computed one-liner       │
│                    alongside raw data. Saves tokens.            │
│                                                                 │
│  3. NO THROWS   ── Return { success: false, message: "..." }    │
│                    instead of throwing. LLM can relay errors.   │
│                                                                 │
│  4. CAP RESULTS ── Always use a limit param. Never return       │
│                    unbounded arrays to the LLM.                 │
└─────────────────────────────────────────────────────────────────┘

Write the description like you're training a new colleague:

// ❌ Vague — LLM won't know when to call this
@AITool( "Gets order information" )

// ✅ Clear — tells the LLM exactly when and what
@AITool( "Retrieve a single order by order ID. Use first when a customer mentions
          a specific order number. Do not call without an explicit order ID." )

Core Concept 2: Memory

Memory is what separates a stateful agent from a stateless API call. Without memory, every message is processed in isolation. With memory, the agent carries the full conversation thread.

┌────────────────────────────────────────────────────────────────┐
│               Without Memory  vs  With Memory                  │
│                                                                │
│  WITHOUT                       WITH                            │
│  ──────────────────            ────────────────────            │
│                                                                │
│  Turn 1:                       Turn 1:                         │
│  User: "My order is late"      User: "My order is late"        │
│  Agent: "Which order?"         Agent: "Which order?"           │
│                                                                │
│  Turn 2:                       Turn 2:                         │
│  User: "ORD-78291"             User: "ORD-78291"               │
│  Agent: "Which order?" ❌       Agent: [looks up ORD-78291] ✅ │
│                                                                │
│  Each call is isolated.        Full context is preserved.      │
└────────────────────────────────────────────────────────────────┘

BoxLang AI ships 20+ memory types. Here are the three you'll use most.

Window Memory — Short-Term Conversation History

Window memory keeps the last N messages. It's the minimum you need for a coherent conversation:

memory = aiMemory( "window", config: { maxMessages: 20 } )

What the memory stores as a conversation builds:

After Turn 1:
┌─────────────────────────────────────────────────────┐
│  user      │ "Where is order #ORD-78291?"           │
│  assistant │ "Your order is in transit..."          │
└─────────────────────────────────────────────────────┘

After Turn 2:
┌─────────────────────────────────────────────────────┐
│  user      │ "Where is order #ORD-78291?"           │
│  assistant │ "Your order is in transit..."          │
│  user      │ "When exactly will it arrive?"         │
│  assistant │ "It's estimated to arrive April 4th."  │
└─────────────────────────────────────────────────────┘

Without memory, "When exactly will it arrive?" has no context — "it" refers to nothing. With memory, the agent knows what "it" means.

Cache Memory — Multi-Tenant Production

For web applications serving multiple users, you need one agent instance that's safe across concurrent requests:

memory = aiMemory( "cache" )

Every memory operation accepts userId and conversationId to route each read/write to the right isolated conversation:

┌──────────────────────────────────────────────────────────────┐
│              One Memory Instance, Many Users                 │
│                                                              │
│  ┌──────────┐                                                │
│  │  Alice   │──► add( msg, userId:"alice", convId:"t-101" )  │
│  └──────────┘                    │                           │
│                                  ▼                           │
│                         ┌────────────────┐                   │
│                         │  Cache Memory  │                   │
│                         │  ──────────── │                    │
│  ┌──────────┐           │  alice/t-101  │                    │
│  │   Bob    │──────────►│  bob/t-102    │                    │
│  └──────────┘           │  carol/t-103  │                    │
│                         └────────────────┘                   │
│                                  │                           │
│  getAll( userId:"alice" ) ───────┘  Returns ONLY Alice's     │
│                                     messages. Bob isolated.  │
└──────────────────────────────────────────────────────────────┘

When you pass userId and conversationId through agent.run() options, they flow automatically to all memory operations — no explicit wiring needed:

// Same agent instance, fully isolated per user
agent.run( "My order is late.", {}, { userId: "alice@example.com", conversationId: "ticket-101" } )
agent.run( "I need a refund.",  {}, { userId: "bob@example.com",   conversationId: "ticket-102" } )

No per-user agent factories. No thread-local hacks. One instance handles thousands of concurrent users safely.

Summary Memory — Long Conversations

For long support sessions, summary memory auto-compresses old messages to preserve context without token bloat:

memory = aiMemory( "summary", config: {
    maxMessages      : 40,
    summaryThreshold : 20,
    summaryModel     : "gpt-4o-mini"   // use a cheap model for summarization
} )
              How Summary Memory Works

Messages 1-20 accumulate normally...

At message 21:
┌──────────────────────────────────────────────────┐
│  Messages 1–20  ──► LLM summarizes ──►           │
│  "Customer reported damaged item on order        │
│   ORD-78291. Refund of $89.99 discussed."        │
└──────────────────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────────────────┐
│  [SUMMARY]  +  Messages 21–40                    │
│  Full context preserved, fraction of the tokens  │
└──────────────────────────────────────────────────┘

Core Concept 3: The Agent

With tools and memory defined, the agent is the piece that ties them together. In BoxLang AI, aiAgent() is a single BIF call that gives you a fully autonomous agent.

┌──────────────────────────────────────────────────────────────┐
│                    The Agent is the Glue                     │
│                                                              │
│   ┌──────────┐   ┌──────────┐   ┌──────────┐                 │
│   │  Tools   │   │  Memory  │   │  Skills  │                 │
│   └────┬─────┘   └────┬─────┘   └────┬─────┘                 │
│        │              │              │                       │
│        └──────────────┼──────────────┘                       │
│                       │                                      │
│                  ┌────▼─────┐                                │
│                  │  Agent   │◄── Instructions                │
│                  │          │◄── Middleware                  │
│                  └────┬─────┘                                │
│                       │                                      │
│                  ┌────▼─────┐                                │
│                  │   LLM    │  (any of 17 providers)         │
│                  └──────────┘                                │
└──────────────────────────────────────────────────────────────┘

The Simplest Possible Agent

// Window memory by default with 20 messages
agent = aiAgent(
    name   : "SupportBot",
    tools  : [ getOrderTool, searchOrdersTool, issueRefundTool ]
)

response = agent.run( "Where is order #ORD-78291?" )
println( response )

That's it. The agent handles the full reasoning loop: deciding when to call tools, passing results back to the LLM, and producing a final response.

Giving the Agent an Identity

A well-defined description and instructions dramatically improve agent behavior:

agent = aiAgent(
    name         : "SupportBot",
    description  : "Customer support specialist for Acme Store. Expert in orders, shipping, returns, and product questions.",
    instructions : "
        You are a friendly and efficient customer support agent.
        Always look up order details before discussing specific orders.
        Confirm refund requests explicitly before calling issue_refund.
        Lead with the direct answer, then add supporting detail.
        If you cannot resolve an issue, offer to escalate to a human agent.
    ",
    tools        : [ getOrderTool, searchOrdersTool, issueRefundTool ],
    memory       : aiMemory( "cache" )
)

The Agent Run Lifecycle

┌──────────────────────────────────────────────────────────────┐
│                  Agent Run Lifecycle                         │
│                                                              │
│  agent.run( "My order is late" )                             │
│        │                                                     │
│        ▼                                                     │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  1. Resolve userId / conversationId for this call   │     │
│  │  2. Build system message (description + instructions│     │
│  │     + skills + tool list)                           │     │
│  │  3. Load conversation history from memory           │     │
│  │  4. Assemble: [system, ...history, user message]    │     │
│  └────────────────────┬────────────────────────────────┘     │
│                       │                                      │
│                       ▼                                      │
│              ┌────────────────┐                              │
│              │   LLM Call     │                              │
│              └───────┬────────┘                              │
│                      │                                       │
│              Tool calls?                                     │
│              ┌───────┴────────┐                              │
│             YES               NO                             │
│              │                │                              │
│              ▼                ▼                              │
│       ┌────────────┐   ┌────────────────┐                    │
│       │ Execute    │   │ Store in memory│                    │
│       │ each tool  │   │ Return answer  │                    │
│       └─────┬──────┘   └────────────────┘                    │
│             │                                                │
│             └──► back to LLM Call (loop)                     │
│                                                              │
└──────────────────────────────────────────────────────────────┘

This loop is what makes the agent autonomous — it keeps calling tools until it has everything it needs to produce a final answer.

How to Put It All Together

Here's the complete SupportBot:

// SupportBot.bx
import bxModules.bxai.models.middleware.core.LoggingMiddleware;
import bxModules.bxai.models.middleware.core.GuardrailMiddleware;
import bxModules.bxai.models.middleware.core.MaxToolCallsMiddleware;

class {

    property name="agent";

    /**
     * Wire up the agent with tools, memory, and middleware.
     *
     * @orderService   Your order data service
     * @kbVectorMemory Vector memory backed by your knowledge base (optional)
     */
    function init( required any orderService, any kbVectorMemory ) {
        // 1. Register tools by scanning the OrderTools class
        aiToolRegistry().scan( new OrderTools( arguments.orderService ), "support" )

        // 2. Build the agent
        variables.agent = aiAgent(
            name        : "SupportBot",
            description : "Customer support specialist for Acme Store.",
            instructions: "
                You are a friendly and efficient customer support agent.
                Always call get_order before discussing a specific order.
                Confirm refunds explicitly before calling issue_refund.
                Lead with the direct answer, then add supporting detail.
                If you cannot resolve an issue, offer to escalate.
            ",
            tools       : [ "get_order@support", "search_orders@support", "issue_refund@support", "now@bxai" ],
            memory      : aiMemory( "cache" ),
            middleware  : [
                new LoggingMiddleware( logToConsole: true, prefix: "[SupportBot]" ),
                new GuardrailMiddleware( blockedTools: [ "delete_order" ] ),
                new MaxToolCallsMiddleware( maxCalls: 8 )
            ]
        )

        // 3. Optionally seed with a knowledge base for RAG
        if ( !isNull( arguments.kbVectorMemory ) ) {
            variables.agent.addMemory( arguments.kbVectorMemory )
        }

        return this
    }

    /**
     * Handle a customer message — returns the full response string.
     */
    string function handle(
        required string message,
        required string userId,
        required string conversationId
    ) {
        return variables.agent.run(
            arguments.message,
            {},
            {
                userId        : arguments.userId,
                conversationId: arguments.conversationId
            }
        )
    }

}

What the Middleware Does

┌────────────────────────────────────────────────────────────────┐
│                  Middleware Stack                              │
│                                                                │
│  Every agent.run() call passes through:                        │
│                                                                │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  LoggingMiddleware   — logs every LLM call + tool call   │  │
│  │  GuardrailMiddleware — blocks forbidden tools (delete_*) │  │
│  │  MaxToolCallsMiddleware — stops runaway loops at 8 calls │  │
│  └──────────────────────────────────────────────────────────┘  │
│           │             │                │                     │
│           ▼             ▼                ▼                     │
│       ai.log      reject call       cancel run                 │
│                   with error        gracefully                 │
└────────────────────────────────────────────────────────────────┘

LoggingMiddleware logs every agent run, LLM call, and tool invocation to BoxLang's ai log file. In development you'll see exactly what the agent is doing. In production, disable logToConsole and write to the log for observability.

GuardrailMiddleware blocks delete_order permanently — even if the LLM somehow decides to call it. Defense-in-depth for high-stakes operations.

MaxToolCallsMiddleware prevents runaway agents. If the agent gets stuck in a tool-calling loop, it hits the cap and stops with a clear error rather than burning tokens indefinitely.

Streaming Responses

For web UIs and real-time applications, you want the agent's response to appear token-by-token as it's generated — like typing. This is what makes AI feel alive rather than frozen.

BoxLang AI supports streaming at every level: direct model calls, agent runs, and web responses.

How Streaming Works

┌──────────────────────────────────────────────────────────────┐
│                   Streaming vs Blocking                      │
│                                                              │
│  BLOCKING (default)                                          │
│  ──────────────────                                          │
│  User sends message                                          │
│  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  (waiting 2–8 seconds)     │
│  Full response arrives at once                               │
│                                                              │
│  STREAMING                                                   │
│  ─────────                                                   │
│  User sends message                                          │
│  "Your" ► " order" ► " #ORD" ► "-78291" ► " is" ► ...        │
│  Response appears immediately, token by token                │
└──────────────────────────────────────────────────────────────┘

Simple Streaming with aiChatStream()

For basic streaming without an agent:

// Stream a response token by token
aiChatStream(
    messages : "Explain how BoxLang AI handles tool calling",
    callback : chunk => {
        // Each chunk contains a delta with partial content
        var token = chunk.choices?.first()?.delta?.content ?: ""
        if ( token.len() ) {
            writeOutput( token )
            bx:flush;  // push each token to the browser immediately
        }
    },
    params   : { model: "gpt-4o" }
)

Agent Streaming with agent.stream()

The stream() method on AiAgent works exactly like run() but delivers the response token by token. Tool calls still execute synchronously under the hood — the streaming applies to the final text response:

// SupportBot.bx — add this alongside the handle() method
void function handleStream(
    required string   message,
    required string   userId,
    required string   conversationId,
    required function onChunk
) {
    variables.agent.stream(
        onChunk : arguments.onChunk,
        input   : arguments.message,
        options : {
            userId        : arguments.userId,
            conversationId: arguments.conversationId
        }
    )
}

Streaming to a Web Browser (BoxLang Web)

Here's how to wire streaming to a real HTTP response — tokens pushed to the browser as they arrive:

// handlers/SupportStreamHandler.bx
class {

    property name="supportBot" inject="SupportBot";

    function stream( event, rc, prc ) {
        var userId         = auth.getCurrentUser().getEmail()
        var conversationId = rc.ticketId
        
        // Use BoxLang's Native SSE Streamer
        SSE(
            callback          : ( emitter ) => {
                supportBot.handleStream(
                    message        : rc.message,
                    userId         : userId,
                    conversationId : conversationId,
                    onChunk        : chunk => {
                        if ( emitter.isClosed() ) {
                            return
                        }
                        var token = chunk.choices?.first()?.delta?.content ?: ""
                        if ( token.len() ) {
                            emitter.send( token, "token" )
                        }
                    }
                )
                emitter.send( { complete: true }, "done" )
                emitter.close()
            },
            keepAliveInterval : 30000,
            cors              : ""
        )
    }

}

Consuming the Stream on the Frontend

On the client side, use the standard EventSource API or fetch with a readable stream:

// JavaScript — connect to the SSE stream
const eventSource = new EventSource(
    `/support/stream?ticketId=${Setting: ticketId not found}&message=${Setting: encodeURIComponent(message) not found}`
);

const responseEl = document.getElementById( "agent-response" );

eventSource.onmessage = ( event ) => {
    if ( event.data === "[DONE]" ) {
        eventSource.close();
        return;
    }
    // Append each token as it arrives
    responseEl.textContent += event.data;
};

eventSource.onerror = () => eventSource.close();

Streaming with Accumulated Memory

One important detail: even in streaming mode, the full response is stored in memory after the stream completes. The AiAgent.stream() method accumulates tokens internally and saves them when done:

// From AiAgent.bx — the wrapped callback pattern
var accumulated = ""
var wrappedCallback = ( chunk ) => {
    var content = chunk.choices?.first()?.delta?.content ?: ""
    accumulated &= content        // accumulate for memory
    userOnChunk( chunk )          // forward to your callback
}

// After streaming completes, store the full response
storeInMemory( userMessage, { role: "assistant", content: accumulated }, userId, conversationId )

This means streaming and memory work seamlessly together — the user sees tokens as they arrive, and the next turn has the full conversation history.

When to Use Streaming

┌──────────────────────────────────────────────────────────────┐
│               Streaming Decision Guide                       │
│                                                              │
│  USE streaming when:                                         │
│  • Building a chat UI where responsiveness matters           │
│  • Responses are long (> 2-3 sentences)                      │
│  • You want a "typing" feel for the user                     │
│  • Delivering to a browser over HTTP                         │
│                                                              │
│  USE blocking (agent.run()) when:                            │
│  • Processing in a background job or batch pipeline          │
│  • The caller needs the complete response before proceeding  │
│  • Building an API that returns JSON                         │
│  • Writing tests (deterministic, easier to assert)           │
└──────────────────────────────────────────────────────────────┘

How the Agent Thinks

Let's trace exactly what happens for a real multi-step request: "My order #ORD-78291 arrived damaged. I want a refund."

┌──────────────────────────────────────────────────────────────┐
│              Full Agent Execution Trace                      │
│                                                              │
│  USER: "My order #ORD-78291 arrived damaged. I want          │
│         a refund."                                           │
│         │                                                    │
│         ▼                                                    │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  LLM CALL 1                                         │     │
│  │  "Customer wants refund. Look up order first."      │     │
│  │  → tool_call: get_order( "ORD-78291" )              │     │
│  └───────────────────┬─────────────────────────────────┘     │
│                      │                                       │
│         ▼            ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  TOOL: get_order                                    │     │
│  │  { found: true, status: "Delivered",                │     │
│  │    total: 89.99, summary: "Order #ORD-78291..." }   │     │
│  └───────────────────┬─────────────────────────────────┘     │
│                      │                                       │
│                      ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  LLM CALL 2                                         │     │
│  │  "Order confirmed. Instructions say confirm         │     │
│  │  before issuing refund."                            │     │
│  │  → text: "Can you confirm the $89.99 refund?"       │     │
│  └───────────────────┬─────────────────────────────────┘     │
│                      │                                       │
│  USER: "Yes, please go ahead."                               │
│                      │                                       │
│                      ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  LLM CALL 3                                         │     │
│  │  "Customer confirmed. Issue the refund."            │     │
│  │  → tool_call: issue_refund( "ORD-78291",            │     │
│  │                             "Item arrived damaged" )│     │
│  └───────────────────┬─────────────────────────────────┘     │
│                      │                                       │
│                      ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  TOOL: issue_refund                                 │     │
│  │  { success: true, refundId: "REF-44821",            │     │
│  │    amount: 89.99, processingDays: 5 }               │     │
│  └───────────────────┬─────────────────────────────────┘     │
│                      │                                       │
│                      ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  LLM CALL 4                                         │     │
│  │  "Refund confirmed. Compose final response."        │     │
│  │  → text: "Your refund of $89.99 has been            │     │
│  │           processed (REF-44821)..."                 │     │
│  └──────────────────────────────────────────────────── ┘     │
│                      │                                       │
│                      ▼                                       │
│  ┌─────────────────────────────────────────────────────┐     │
│  │  STORE in memory (scoped to this user + ticket)     │     │
│  │  RETURN to caller                                   │     │
│  └─────────────────────────────────────────────────────┘     │
└──────────────────────────────────────────────────────────────┘

The agent confirms before acting (because the instructions say to), executes the tool only after explicit confirmation, and builds the full response from the tool result. This is the multi-step reasoning that makes agents genuinely useful.

What the conversation history looks like at the end:

┌────────────────────────────────────────────────────────────┐
│  Role        │  Content                                    │
├──────────────┼─────────────────────────────────────────────┤
│  system      │  "You are SupportBot..."                    │
│  user        │  "My order arrived damaged..."              │
│  assistant   │  [tool_call: get_order]                     │
│  tool        │  { found:true, status:"Delivered"... }      │
│  assistant   │  "Can you confirm the $89.99 refund?"       │
│  user        │  "Yes, please go ahead."                    │
│  assistant   │  [tool_call: issue_refund]                  │
│  tool        │  { success:true, refundId:"REF-44821"... }  │
│  assistant   │  "Your refund of $89.99 has been issued..." │
└────────────────────────────────────────────────────────────┘

Going Further

The SupportBot above covers the essentials. Here's what to add for production.

Adding a Knowledge Base (RAG)

Ingest your documentation into vector memory and the agent retrieves relevant content automatically before answering:

// One-time ingestion (run when docs change)
vectorMemory = aiMemory( "chroma", config: {
    collection       : "support_kb",
    embeddingProvider: "openai",
    embeddingModel   : "text-embedding-3-small"
} )

result = aiDocuments(
    source : "/knowledge-base",
    config : { type: "directory", recursive: true, extensions: [ "md", "txt" ] }
).toMemory(
    memory  : vectorMemory,
    options : { chunkSize: 800, overlap: 150 }
)
println( "Loaded #result.documentsIn# docs → #result.chunksOut# chunks" )
┌──────────────────────────────────────────────────────────────┐
│                    RAG Pipeline                              │
│                                                              │
│  INGESTION (run once)                                        │
│  ─────────────────────────────────────────────────────────   │
│  /knowledge-base/*.md                                        │
│        │                                                     │
│        ▼                                                     │
│  aiDocuments() ──► chunk ──► embed ──► store in ChromaDB     │
│                                                              │
│  QUERY (every agent.run())                                   │
│  ─────────────────────────────────────────────────────────   │
│  User: "What is your return policy?"                         │
│        │                                                     │
│        ▼                                                     │
│  Vector search: find top-5 semantically similar chunks       │
│        │                                                     │
│        ▼                                                     │
│  Inject chunks into LLM context                              │
│        │                                                     │
│        ▼                                                     │
│  LLM answers from YOUR actual docs, not hallucinations       │
└──────────────────────────────────────────────────────────────┘

Human-in-the-Loop Approvals

For refunds above a threshold, require a supervisor to approve before the refund executes:

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

agent = aiAgent(
    name       : "SupportBot",
    middleware : [
        new LoggingMiddleware(),
        new GuardrailMiddleware( blockedTools: [ "delete_order" ] ),
        new MaxToolCallsMiddleware( maxCalls: 8 ),
        new HumanInTheLoopMiddleware(
            mode                  : "web",
            toolsRequiringApproval: [ "issue_refund" ]
        )
    ],
    checkpointer: aiMemory( "cache" )
)
┌──────────────────────────────────────────────────────────────┐
│            Human-in-the-Loop Flow                            │
│                                                              │
│  Agent reaches issue_refund tool call                        │
│        │                                                     │
│        ▼                                                     │
│  HumanInTheLoopMiddleware intercepts                         │
│        │                                                     │
│        ▼                                                     │
│  result.isSuspended() == true                                │
│  Agent saves checkpoint to cache memory                      │
│        │                                                     │
│        ▼                                                     │
│  Your code notifies supervisor (Slack, email, dashboard)     │
│        │                                                     │
│        ▼                                                     │
│  Supervisor approves / rejects / edits args                  │
│        │                                                     │
│        ├── approve ──► agent.resume( "approve", threadId )   │
│        ├── reject  ──► agent.resume( "reject",  threadId )   │
│        └── edit    ──► agent.resume( "edit", threadId,       │
│                             { correctedArgs: { amount:100 }} │
└──────────────────────────────────────────────────────────────┘

Multi-Agent Escalation

For complex issues, automatically delegate to a specialist:

billingAgent = aiAgent(
    name        : "BillingSpecialist",
    description : "Expert in billing disputes, chargebacks, and payment issues",
    tools       : [ "get_payment_history@billing", "dispute_charge@billing" ]
)

// SupportBot gets a delegate_to_billing-specialist tool automatically
supportBot = aiAgent(
    name      : "SupportBot",
    subAgents : [ billingAgent ]
)
┌──────────────────────────────────────────────────────────────┐
│               Multi-Agent Hierarchy                          │
│                                                              │
│            ┌─────────────────┐                               │
│            │   SupportBot    │  (coordinator)                │
│            │  (root agent)   │                               │
│            └────────┬────────┘                               │
│                     │                                        │
│          ┌──────────┴───────────┐                            │
│          │                      │                            │
│  ┌───────┴───────┐    ┌─────────┴──────────┐                 │
│  │   Billing     │    │     Returns &      │                 │
│  │  Specialist   │    │     Shipping       │                 │
│  └───────────────┘    └────────────────────┘                 │
│                                                              │
│  Each sub-agent appears as a "delegate_to_*" tool.           │
│  The LLM decides when to delegate — no routing code needed.  │
└──────────────────────────────────────────────────────────────┘

Conclusion

Building an AI agent with BoxLang AI comes down to three concepts:

┌──────────────────────────────────────────────────────────────┐
│                  The Three Core Concepts                     │
│                                                              │
│  1. TOOLS    ──  Functions your agent can call               │
│                  @AITool annotation or aiTool() BIF          │
│                  Registered once, referenced by name         │
│                                                              │
│  2. MEMORY   ──  Conversation history that makes it          │
│                  stateful and multi-tenant safe              │
│                  window / cache / summary / vector           │
│                                                              │
│  3. AGENT    ──  The reasoning loop that ties it together    │
│                  aiAgent() with instructions + middleware    │
│                  Handles the tool-call loop automatically    │
└──────────────────────────────────────────────────────────────┘

The framework handles the hard parts: the tool-calling loop, memory isolation, provider differences, lifecycle events, and cross-cutting concerns like logging and rate limiting. You focus on your domain logic — the tools that do the actual work.

The full SupportBot example shows how these pieces combine in a real application. The same patterns apply to any domain: financial assistants, developer tools, data analysis agents, document processors — whatever problem you're solving, the architecture is the same.

Resources

📖 BoxLang AI Documentation
🐙 BoxLang AI GitHub
🎓 AI BootCamp — hands-on course covering all concepts in this guide
💬 BoxLang Community Slack
📦 ForgeBox Package

# Start building
install-bx-module bx-ai
boxlang my-agent.bxs

The post How to Develop AI Agents Using BoxLang AI: A Practical Guide appeared first on foojay.

]]>
https://foojay.io/today/how-to-develop-ai-agents-using-boxlang-ai-a-practical-guide/feed/ 0
BoxLang AI Deep Dive — Part 6 of 7: Memory Systems & RAG — Building AI That Remembers https://foojay.io/today/boxlang-ai-deep-dive-part-6-of-7-memory-systems-rag-building-ai-that-remembers/ https://foojay.io/today/boxlang-ai-deep-dive-part-6-of-7-memory-systems-rag-building-ai-that-remembers/#respond Tue, 05 May 2026 15:10:15 +0000 https://foojay.io/?p=123634 Table of Contents 🧠 Two Categories of Memory📋 Standard Memory Types Summary Memory — How It Actually Works 🔍 Vector Memory Types Hybrid Memory — The Best of Both 🏢 Per-Call Multi-Tenant Identity Routing📚 Document Loaders🔗 Building a Complete RAG ...

The post BoxLang AI Deep Dive — Part 6 of 7: Memory Systems & RAG — Building AI That Remembers appeared first on foojay.

]]>

Table of Contents
🧠 Two Categories of Memory📋 Standard Memory Types

🔍 Vector Memory Types

🏢 Per-Call Multi-Tenant Identity Routing📚 Document Loaders🔗 Building a Complete RAG Pipeline

🔧 Token Management🏗 Multiple Memories Per Agent📦 The aiPopulate() BIF — Structured Memory Without Live CallsWhat's Next


BoxLang AI 3.0 Series · Part 6 of 7

A chatbot with no memory isn't a conversation — it's a series of isolated queries. Every message starts from scratch. The user has to re-explain who they are, what they're working on, and what was just said. It's exhausting, and it signals that the AI isn't really listening.

Memory is what separates a useful AI application from a toy. BoxLang AI ships with one of the most comprehensive memory systems in any AI framework — 20+ memory types across two major categories, vector embedding support for semantic retrieval, 30+ document loaders for RAG pipelines, and a per-call identity routing system that makes multi-tenant applications safe by default.

This post is a complete tour.

🧠 Two Categories of Memory

           +-----------------------------------+
           |         BoxLang AI Memory         |
           +-----------------------------------+
                        /           \
                       /             \
                      v               v

+--------------------------------+   +--------------------------------+
|        Standard Memory         |   |         Vector Memory          |
+--------------------------------+   +--------------------------------+
| Stores conversation history    |   | Stores semantic knowledge      |
| Sequential message thread      |   | Embeddings + retrieval         |
| Retrieves by recency/order     |   | Retrieves by meaning           |
| Example: remember prior fact   |   | Example: RAG knowledge lookup  |
+--------------------------------+   +--------------------------------+

                      \               /
                       \             /
                        v           v

         +-------------------------------------------+
         | Shared abstraction and usage model        |
         +-------------------------------------------+
         | IAiMemory interface                       |
         | aiMemory() BIF                            |
         | Per-call identity routing                 |
         | Minimal app-code changes between both     |
         +-------------------------------------------+

BoxLang AI memory breaks into two fundamentally different categories, solving two different problems.

Standard Memory stores conversation history — the sequential messages between user and assistant. It's what lets the agent remember "my name is Luis" from three messages ago.

Vector Memory stores semantic knowledge — embeddings of documents, past conversations, or domain content that can be retrieved by meaning, not by recency. It's what enables RAG: "find the three most relevant passages from our knowledge base for this query."

Both categories share the same IAiMemory interface, the same aiMemory() BIF, and the same per-call identity routing — your application code barely changes between them.

📋 Standard Memory Types

Create any memory with our lovely global function: aiMemory( type, config: {} ). Our default memory type is a window memory of 20 messages:

// Window memory — keeps the last N messages
mem = aiMemory( "window", config: { maxMessages: 20 } )

// Summary memory — auto-summarizes old messages to preserve context
mem = aiMemory( "summary", config: {
    maxMessages      : 30,
    summaryThreshold : 15,
    summaryModel     : "gpt-4o-mini"
} )

// Cache memory — CacheBox-backed, distributed-friendly
mem = aiMemory( "cache", config: { cacheName: "aiMemory" } )

// Session memory — scoped to the current web session
mem = aiMemory( "session" )

// File memory — persisted to disk for audit trails
mem = aiMemory( "file", config: { filePath: "/logs/conversations/" } )

// JDBC memory — stored in a database for enterprise multi-user scenarios
mem = aiMemory( "jdbc", config: {
    datasource : "myDB",
    table      : "ai_conversations"
} )
Type Best For
window Quick chats, cost-conscious apps, stateless APIs
summary Long conversations where context must survive message limits
session Multi-page web applications with PHP/BoxLang sessions
file Audit trails, offline inspection, long-term storage
cache Distributed applications, multi-server deployments
jdbc Enterprise multi-user systems, full persistence

Summary Memory — How It Actually Works

The summary type deserves special attention. When the message count exceeds summaryThreshold, it calls the configured LLM to produce a one-paragraph summary of the oldest messages, replaces them with that summary as a single system message, then continues accumulating. Conversation context survives without the token cost of carrying the full history.

agent = aiAgent(
    name   : "support-bot",
    memory : aiMemory( "summary", config: {
        maxMessages      : 40,    // keep up to 40 messages
        summaryThreshold : 20,    // summarize when we hit 20
        summaryModel     : "gpt-4o-mini"  // use a cheap model for summarization
    } )
)

🔍 Vector Memory Types

Vector memory stores embeddings and retrieves by semantic similarity — the right tool when "find relevant context" matters more than "recall what was said recently."

// In-memory vectors — development and small datasets
mem = aiMemory( "boxvector" )

// ChromaDB — Python-based vector store
mem = aiMemory( "chroma", config: {
    collection       : "support_docs",
    embeddingProvider: "openai",
    embeddingModel   : "text-embedding-3-small"
} )

// PostgreSQL pgvector — works with your existing Postgres
mem = aiMemory( "postgres", config: {
    datasource       : "myDB",
    table            : "ai_embeddings",
    embeddingProvider: "openai"
} )

// Pinecone — managed cloud vector DB
mem = aiMemory( "pinecone", config: {
    apiKey     : "${Setting: PINECONE_API_KEY not found}",
    index      : "knowledge-base",
    namespace  : "support"
} )

// OpenSearch — AWS OpenSearch or self-hosted
mem = aiMemory( "opensearch", config: {
    host             : "https://my-opensearch:9200",
    index            : "ai_embeddings",
    embeddingProvider: "openai"
} )

Full vector memory roster:

Type Description
boxvector In-memory, development/testing
hybrid Recent window + semantic retrieval combined
chroma ChromaDB integration
postgres PostgreSQL pgvector
mysql MySQL 9 native vectors
opensearch MySQL 9 native vectors
typesense Fast typo-tolerant search
pinecone Managed cloud vector DB
qdrant High-performance vector store
weaviate GraphQL vector database
milvus Enterprise-scale vector DB

Hybrid Memory — The Best of Both

hybrid combines a recent message window with semantic vector retrieval — you get recency and relevance:

mem = aiMemory( "hybrid", config: {
    recentLimit   : 5,        // keep last 5 messages always
    semanticLimit : 5,        // add 5 semantically relevant past messages
    vectorProvider: "chroma"  // backed by ChromaDB
} )

For most production support-bot or assistant scenarios, hybrid is the sweet spot — recent context for coherence, semantic retrieval for depth.

🏢 Per-Call Multi-Tenant Identity Routing

This is the architectural feature that makes BoxLang AI memory extensible. Memory instances are stateless and safe to use as singletons — userId and conversationId route each operation to the correct isolated conversation. Or you can create memories with seeded identities if you want a specific agent with specific memory; your choice.

Every memory operation accepts optional identity arguments:

sharedMemory = aiMemory( "cache" )

// Operations are fully tenant-isolated
sharedMemory.add( message, userId: "alice", conversationId: "sess-1" )
sharedMemory.add( message, userId: "bob",   conversationId: "sess-2" )

// Retrieval is scoped — alice never sees bob's messages
aliceHistory = sharedMemory.getAll( userId: "alice", conversationId: "sess-1" )
bobHistory   = sharedMemory.getAll( userId: "bob",   conversationId: "sess-2" )

// Clear only alice's conversation
sharedMemory.clear( userId: "alice", conversationId: "sess-1" )

In practice, you pass identity through AiAgent.run() options and it flows automatically to all memory operations:

sharedAgent = aiAgent( name: "support", memory: sharedMemory )

// One agent instance, many concurrent users — fully safe
sharedAgent.run( "Hello, I need help with my order",    {}, { userId: "alice", conversationId: "sess-1" } )
sharedAgent.run( "What did I just ask about?",          {}, { userId: "alice", conversationId: "sess-1" } ) // remembers
sharedAgent.run( "Can you help me reset my password?",  {}, { userId: "bob",   conversationId: "sess-2" } ) // isolated

No per-user agent factories. No thread-local hacks. No shared-state concurrency bugs. One instance, many tenants.

📚 Document Loaders

Document loaders are the ingestion layer for RAG pipelines. They normalize content from 30+ source types into the Document format that vector memory understands.

// Load a single PDF
docs = aiDocuments(
    source : "/path/to/product-manual.pdf",
    config : { type: "pdf" }
).load()

// Load all Markdown files in a directory (recursively)
docs = aiDocuments(
    source : "/knowledge-base",
    config : {
        type       : "directory",
        recursive  : true,
        extensions : [ "md", "txt", "pdf" ]
    }
).load()

// Load a live web page
docs = aiDocuments(
    source : "https://boxlang.ortusbooks.com/getting-started/overview",
    config : { type: "http" }
).load()

// Load from a database query
docs = aiDocuments(
    source : "SELECT title, content FROM articles WHERE published = 1",
    config : { type: "sql", datasource: "myDB" }
).load()

// Crawl an entire website
docs = aiDocuments(
    source : "https://docs.mycompany.com",
    config : {
        type     : "webcrawler",
        maxPages : 200,
        delay    : 500
    }
).load()

Built-in loaders:

Loader Type Handles
TextLoader text .txt, .log
MarkdownLoader markdown .md with header splitting
HTMLLoader html Web pages, strips scripts/styles
CSVLoader csv Rows as documents, column filtering
JSONLoader json Field extraction, array-as-documents
PDFLoader pdf Multi-page, page range selection
XMLLoader xml Structured XML content
LogLoader log Application log files
HTTPLoader http Single URL fetch
FeedLoader feed RSS / Atom feeds
SQLLoader sql Database query results
DirectoryLoader directory Batch file processing
WebCrawlerLoader webcrawler Multi-page crawl

🔗 Building a Complete RAG Pipeline

Here's the full picture — ingest documents into vector memory, then use an agent with that memory to answer questions grounded in your content.

Step 1: Ingest

// Create vector memory backed by ChromaDB
vectorMemory = aiMemory( "chroma", config: {
    collection       : "company_knowledge",
    embeddingProvider: "openai",
    embeddingModel   : "text-embedding-3-small"
} )

// Ingest everything in one call
result = aiDocuments(
    source : "/knowledge-base",
    config : {
        type       : "directory",
        recursive  : true,
        extensions : [ "md", "txt", "pdf" ]
    }
).toMemory(
    memory  : vectorMemory,
    options : { chunkSize: 1000, overlap: 200 }
)

// Rich ingestion report
println( "Documents loaded : #result.documentsIn#" )
println( "Chunks created   : #result.chunksOut#" )
println( "Vectors stored   : #result.stored#" )
println( "Duplicates skipped: #result.deduped#" )
println( "Estimated cost   : $#result.estimatedCost#" )

The toMemory() method handles chunking via aiChunk(), embedding via the configured provider, deduplication, and storage — everything in one fluent call with a detailed report back.

Step 2: Query

// Agent with the same vector memory — retrieves relevant chunks automatically
agent = aiAgent(
    name        : "knowledge-assistant",
    description : "Expert on all company documentation and policies",
    memory      : vectorMemory
)

// The agent retrieves semantically relevant chunks and grounds its answer
response = agent.run(
    "What is our refund policy for enterprise customers?",
    {},
    { userId: "support-team", conversationId: "ticket-12345" }
)

When the agent runs, vector memory retrieves the most semantically similar document chunks for the query and injects them as context before the LLM call. The LLM answers based on your actual content — not hallucinations.

Step 3: Hybrid for Production

For most production RAG scenarios, hybrid memory beats pure vector:

// Combines short-term conversation memory with long-term semantic retrieval
productionMemory = aiMemory( "hybrid", config: {
    recentLimit   : 8,
    semanticLimit : 6,
    vectorProvider: "chroma",
    collection    : "company_knowledge"
} )

agent = aiAgent(
    name   : "enterprise-assistant",
    memory : productionMemory
)

The first 8 messages keep conversations coherent. The semantic layer ensures relevant documentation is always surfaced. Together they handle both "what did I just ask?" and "what does our policy say about X?"

🔧 Token Management

Two BIFs help you reason about context window usage:

// Count tokens before sending (approximate)
tokenCount = aiTokens( "This is the text I want to count", { method: "words" } )

// Chunk a large document for ingestion
chunks = aiChunk( largeText, {
    chunkSize : 1000,  // tokens per chunk
    overlap   : 200    // overlap between chunks for context continuity
} )

aiChunk() is used internally by toMemory(), but you can call it directly when building custom ingestion pipelines.

🏗️ Multiple Memories Per Agent

Agents can have multiple memory instances simultaneously — useful when you want different retention policies for different types of information:

agent = aiAgent(
    name   : "research-assistant",
    memory : [
        // Short-term: current conversation
        aiMemory( "window", config: { maxMessages: 20 } ),
        // Long-term: semantic knowledge base
        aiMemory( "chroma", config: {
            collection       : "research_papers",
            embeddingProvider: "openai"
        } )
    ]
)

// Add another memory dynamically
agent.addMemory( aiMemory( "file", config: { filePath: "/audit/" } ) )

All memories are read from and written to in parallel. Messages retrieved from all memories are merged before each LLM call.

📦 The aiPopulate() BIF — Structured Memory Without Live Calls

One often-overlooked feature: aiPopulate() fills a typed BoxLang class from JSON without making any LLM call. This is essential for caching and testing:

class CustomerProfile {
    property name="name"         type="string";
    property name="tier"         type="string";
    property name="openTickets"  type="numeric";
}

// From a live AI call
profile = aiChat(
    "Extract the customer profile from: John Doe, Gold tier, 3 open tickets",
    { returnFormat: new CustomerProfile() }
)

// Cache it as JSON
cachedJson = jsonSerialize( profile )

// Later — restore the typed object without another LLM call
restoredProfile = aiPopulate( new CustomerProfile(), cachedJson )
println( restoredProfile.getName() ) // "John Doe"

Perfect for: pre-populated test fixtures, cached AI extractions, converting existing JSON data to typed objects.

What's Next

In Part 7 — the final post in the series — we go deep on MCP: how to consume tools from any MCP server, how MCPTool proxies work, and how to expose your own BoxLang functions as an enterprise MCP server with full security, CORS, API key validation, and rate limiting.

📖 Full Documentation 🌐 BoxLang AI Site 📦Install Today: install-bx-module bx-ai 🫶Professional Support

← Previous

Next ->

The post BoxLang AI Deep Dive — Part 6 of 7: Memory Systems & RAG — Building AI That Remembers appeared first on foojay.

]]>
https://foojay.io/today/boxlang-ai-deep-dive-part-6-of-7-memory-systems-rag-building-ai-that-remembers/feed/ 0
BoxLang AI Deep Dive — Part 4 of 7: Middleware — The Missing Layer in Every AI Framework 🧵 https://foojay.io/today/boxlang-ai-deep-dive-part-4-of-7-middleware-the-missing-layer-in-every-ai-framework-%f0%9f%a7%b5/ https://foojay.io/today/boxlang-ai-deep-dive-part-4-of-7-middleware-the-missing-layer-in-every-ai-framework-%f0%9f%a7%b5/#respond Thu, 23 Apr 2026 14:25:53 +0000 https://foojay.io/?p=123456 Table of Contents 🏗️ The Middleware Architecture🎯 AiMiddlewareResult — Typed Flow Control📝 LoggingMiddleware — Instant Observability🔁 RetryMiddleware — Resilience Without Boilerplate🛡️ GuardrailMiddleware — Defense in Depth🙋 HumanInTheLoopMiddleware — Keeping Humans in Control🎙️ FlightRecorderMiddleware — AI Testing Solved🔢 MaxToolCallsMiddleware — Runaway ...

The post BoxLang AI Deep Dive — Part 4 of 7: Middleware — The Missing Layer in Every AI Framework 🧵 appeared first on foojay.

]]>

Table of Contents
🏗 The Middleware Architecture🎯 AiMiddlewareResult — Typed Flow Control📝 LoggingMiddleware — Instant Observability🔁 RetryMiddleware — Resilience Without Boilerplate🛡 GuardrailMiddleware — Defense in Depth🙋 HumanInTheLoopMiddleware — Keeping Humans in Control🎙 FlightRecorderMiddleware — AI Testing Solved🔢 MaxToolCallsMiddleware — Runaway Agent Prevention✍ Writing Your Own Middleware🚀 Composing MiddlewareWhat's Next


BoxLang AI 3.0 Series · Part 4 of 7

Here's the question every team eventually asks about their AI agents: how do we test these things?

Agents make live LLM calls. They invoke real tools. They have non-deterministic outputs. Standard unit testing approaches fall apart. You can't mock every provider. You can't replay a conversation from three weeks ago. You can't confidently tell stakeholders that the agent you deployed today behaves the same way it did when you signed off on it.

And before testing, there's production: how do you add logging without touching provider code? How do you retry transient failures without wrapping every call? How do you block dangerous tool invocations without forking the agent logic?

BoxLang AI 3.0 solves all of this with middleware. Six battle-tested middleware classes ship out of the box, covering the most common cross-cutting concerns. And if none of them fit your use case exactly, writing your own is a matter of extending one class or defining a struct of closures.

🏗️ The Middleware Architecture

Middleware sits between the agent's run() call and the actual LLM invocations and tool calls. Both AiModel and AiAgent support it. When an agent runs, its middleware is prepended to the model's middleware — agent hooks fire first, then model hooks.

There are two hook styles:

Sequential hooks — fire in registration order (or reverse for after* hooks). Return AiMiddlewareResult to control flow.

Hook Fires Direction
beforeAgentRun( context ) Before agent starts Forward
afterAgentRun( context ) After agent completes Reverse
beforeLLMCall( context ) Before each LLM call Forward
afterLLMCall( context ) After each LLM call Reverse
beforeToolCall( context ) Before each tool invocation Forward
afterToolCall( context ) After each tool returns Reverse
onError( context ) When any hook throws

Wrap hooks — nested closures. Call handler() to proceed, intercept the result.

Hook Purpose
wrapLLMCall( context, handler ) Surround each LLM provider call
wrapToolCall( context, handler ) Surround each tool invocation

🎯 AiMiddlewareResult — Typed Flow Control

Every sequential hook must return an AiMiddlewareResult. The static factory methods make this expressive:

import bxModules.bxai.models.middleware.AiMiddlewareResult;

// Continue normally — chain proceeds
return AiMiddlewareResult.continue()

// Stop everything immediately
return AiMiddlewareResult.cancel( "Rate limit exceeded for this tenant." )

// Human approved — used by HITL middleware
return AiMiddlewareResult.approve()

// Human rejected — terminal, stops the chain
return AiMiddlewareResult.reject( "Operator rejected: amounts over $1000 require VP approval." )

// Human edited the tool args — patched args flow to the tool
return AiMiddlewareResult.edit( { correctedArgs: { amount: 100 } } )

// Suspend for async human review — terminal
return AiMiddlewareResult.suspend( { toolName: "transferFunds", args: toolArgs } )

Terminal results (cancel, reject, suspend) stop the chain immediately. Non-terminal results continue to the next middleware.

// Predicates for checking results
result.isContinue()   // chain continues
result.isCancelled()  // was stopped
result.isApproved()   // human approved
result.isRejected()   // human rejected (terminal)
result.isEdit()       // args were modified
result.isSuspended()  // waiting for async input (terminal)
result.isTerminal()   // cancelled OR rejected OR suspended

📝 LoggingMiddleware — Instant Observability

Drop this in and every LLM call, tool invocation, agent run start/end, and error gets logged to BoxLang's ai log file and optionally to the console — with zero code changes to your agents:

agent = aiAgent(
    name       : "support-bot",
    middleware : new LoggingMiddleware(
        logToConsole : true,
        logLevel     : "info",
        prefix       : "[SupportBot]"
    )
)

The implementation is a clean example of how sequential hooks compose:

// From LoggingMiddleware.bx
AiMiddlewareResult function beforeAgentRun( required struct context ) {
    emit( "Agent run starting | input: #left( toString( context.input ), 120 )#" )
    return AiMiddlewareResult.continue()
}

AiMiddlewareResult function afterToolCall( required struct context ) {
    var toolName = context.tool?.getName() ?: "unknown"
    emit( "Tool call complete | tool: #toolName# | result: #left( toString( context.result ), 120 )#" )
    return AiMiddlewareResult.continue()
}

AiMiddlewareResult function onError( required struct context ) {
    emit( "Error in phase '#context.phase#': #context.error?.message#", "error" )
    return AiMiddlewareResult.continue()  // don't stop the chain on logging errors
}

Options:

Option Default Description
logToFile true Write to BoxLang ai log
logToConsole false Also print to stdout
logLevel "info" info, debug, warning, error
prefix "[AI Middleware]" Prepended to every message

🔁 RetryMiddleware — Resilience Without Boilerplate

LLM providers have rate limits. Networks have transient failures. RetryMiddleware wraps both LLM calls and tool calls with exponential backoff — transparently, without any code in your tools or agents:

agent = aiAgent(
    name       : "analyst",
    middleware : new RetryMiddleware(
        maxRetries        : 5,
        initialDelay      : 2000,
        backoffMultiplier : 1.5,
        maxDelay          : 30000
    )
)

It uses wrapLLMCall and wrapToolCall hooks — the outer wrap catches exceptions, sleeps, and retries up to maxRetries times. Non-retryable exceptions (like InvalidInput or MaxInteractionsExceeded) surface immediately:

Option Default Description
maxRetries 3 Attempts after first failure
initialDelay 1000 First retry delay in ms
backoffMultiplier 2 Multiplier applied per failure
maxDelay 30000 Hard cap on delay
nonRetryableTypes "InvalidInput,MaxInteractionsExceeded" Exception types to skip

🛡️ GuardrailMiddleware — Defense in Depth

Block dangerous tools entirely, or reject tool calls whose arguments match regex patterns — before they ever reach the tool:

guardrail = new GuardrailMiddleware(
    blockedTools : [ "deleteRecord", "dropTable", "truncateAll" ],
    argPatterns  : {
        runSql  : [ { query: "(?i)drop|truncate|delete" } ],
        sendMail: [ { to: "@competitor\\.com$" } ]
    }
)

agent = aiAgent( name: "db-assistant", middleware: guardrail )

The hook fires in beforeToolCall — it checks the tool name against blockedTools first, then validates each argument against the configured regex patterns:

// From GuardrailMiddleware.bx
AiMiddlewareResult function beforeToolCall( required struct context ) {
    var toolName = context.toolName ?: (context.tool?.getName() ?: "")

    // 1. Blocked tool list check
    if ( variables.blockedTools.findNoCase( toolName ) > 0 ) {
        return AiMiddlewareResult.reject(
            "GuardrailMiddleware: tool '#toolName#' is in the blocked tools list."
        )
    }

    // 2. Argument pattern checks
    if ( variables.argPatterns.keyExists( toolName ) ) {
        // ... check each rule against the resolved tool arguments
    }

    return AiMiddlewareResult.continue()
}
Option Default Description
blockedTools [] Tool names always rejected (case-insensitive)
argPatterns {} { toolName: [{ paramName: "regex" }] }

🙋 HumanInTheLoopMiddleware — Keeping Humans in Control

This middleware intercepts specific tool calls and requires a human to approve, reject, or edit before execution proceeds. Two modes, two very different use cases.

CLI mode — blocks on stdin. Perfect for local scripts, automation tools, and development workflows:

agent = aiAgent(
    name       : "finance-bot",
    middleware : new HumanInTheLoopMiddleware(
        toolsRequiringApproval : [ "transferFunds", "placeOrder" ],
        showArguments          : true
    )
)

When the LLM calls transferFunds, the terminal shows:

╔══════════════════════════════════════════════════╗
║         HUMAN APPROVAL REQUIRED                  ║
╚══════════════════════════════════════════════════╝
 Tool: transferFunds
 Args: {"amount": 5000, "account": "12345"}

 [A]pprove  [R]eject  [Q]uit
 Decision:

Web mode — suspends the run and returns an AiMiddlewareResult.suspend(). The calling code checkpoints state and presents the approval request asynchronously — via email, Slack, a web UI, whatever fits your workflow:

agent = aiAgent(
    name        : "finance-bot",
    middleware  : new HumanInTheLoopMiddleware(
        mode                  : "web",
        toolsRequiringApproval: [ "placeOrder" ]
    ),
    checkpointer: aiMemory( "cache" )
)

// First request — the LLM wants to place an order
result = agent.run( "Order 50 units of product SKU-789", {}, { userId: "alice" } )

if ( result.isSuspended() ) {
    // Send approval request to alice's manager via Slack, email, etc.
    notifyManager( result.getData() )
    // Store threadId for resume
    session.pendingApproval = result.getData().threadId
}

// After the manager approves (in a separate request/thread)
agent.resume( "approve", session.pendingApproval )

// Or if they edit the quantity
agent.resume( "edit", session.pendingApproval, { correctedArgs: { quantity: 10 } } )

The resume path in HumanInTheLoopMiddleware reads the _resumeContext injected by AiAgent.resume(), honours the decision, and either continues, rejects, or patches the tool arguments — then clears the context so subsequent tool calls in the same run go through normal HITL flow again.

Option Default Description
toolsRequiringApproval [] Tools needing sign-off
mode "cli" "cli" or "web"
showArguments true Show args in CLI prompt
approvalCallback Custom approval function

🎙️ FlightRecorderMiddleware — AI Testing Solved

This is the one that changes how you think about testing AI agents.

The problem: agent behaviour is non-deterministic. The LLM might phrase something differently each run. The tool call order might vary. Writing assertions against agent output directly is fragile. And running tests against live providers is slow, expensive, and requires network access in CI.

FlightRecorderMiddleware solves this with a record/replay approach. Record a real run once — capturing every LLM round-trip and tool invocation to a JSON fixture file. Then replay that fixture in CI without any live calls.

Three modes:

// RECORD — calls real providers and tools, saves every interaction
agent = aiAgent(
    name       : "weather-bot",
    middleware : new FlightRecorderMiddleware( mode: "record" )
)
agent.run( "What's the weather in London and should I bring an umbrella?" )
// → Writes: .ai/flight-recorder/weather-bot-20260402-143022.json
// REPLAY — zero live calls, fully deterministic
agent = aiAgent(
    name       : "weather-bot",
    middleware : new FlightRecorderMiddleware(
        mode        : "replay",
        fixturePath : "tests/fixtures/weather-bot.json"
    )
)
agent.run( "What's the weather in London and should I bring an umbrella?" )
// → Returns the exact same response as the recorded run
// PASSTHROUGH (default) — no recording, calls pass through normally
agent = aiAgent(
    name       : "weather-bot",
    middleware : new FlightRecorderMiddleware()  // mode: "passthrough"
)

The fixture format — human-readable JSON that you can inspect, edit, and commit to version control:

{
    "version": "1",
    "recordedAt": "2026-04-02T14:30: 22",
    "agentName": "weather-bot",
    "interactions": [
        {
            "seq": 1,
            "type": "llm",
            "request": { "model": "gpt-4o", "messages": [...], "tools": [...] },
            "response": { "choices": [{ "message": { "tool_calls": [...] } }] }
        },
        {
            "seq": 2,
            "type": "tool",
            "toolName": "getWeather",
            "arguments": { "city": "London" },
            "result": "15°C, overcast, 80% chance of rain"
        },
        {
            "seq": 3,
            "type": "llm",
            "request": { ... },
            "response": { "choices": [{ "message": { "content": "Yes, bring an umbrella..." } }] }
        }
    ]
}

One implementation detail worth noting: the recorder flushes to disk after every interaction, not just at the end. This means if your agent crashes mid-run, the partial recording is preserved and can be inspected:

// From FlightRecorderMiddleware.bx
private void function _appendInteraction( required struct interaction ) {
    var seq = variables._tape.interactions.len() + 1
    arguments.interaction.seq = seq
    variables._tape.interactions.append( arguments.interaction )
    _saveSnapshot()  // flush after every interaction — crash-safe
}

Strict vs lenient replay:

// Strict (default): throw on type mismatch — "expecting llm but tape has tool"
new FlightRecorderMiddleware( mode: "replay", strict: true )

// Lenient: skip forward to find next matching interaction type
new FlightRecorderMiddleware( mode: "replay", strict: false )
Option Default Description
mode "passthrough" "passthrough", "record", or "replay"
fixturePath "" Path to fixture file
fixtureDir ".ai/flight-recorder" Auto-generated fixture directory
recordTools true Whether to capture tool interactions
strict true Throw on type mismatch in replay

🔢 MaxToolCallsMiddleware — Runaway Agent Prevention

Simple but essential in production — caps the total number of tool invocations per agent run:

agent = aiAgent(
    name       : "research-bot",
    middleware : new MaxToolCallsMiddleware( maxCalls: 10 )
)

The counter resets at the start of each new run() call. If the cap is hit mid-run, the chain is cancelled with a clear error message. Essential for preventing infinite tool call loops in complex multi-step reasoning tasks.

✍️ Writing Your Own Middleware

Two approaches, depending on how much structure you want.

Struct of closures — lightweight, no class needed:

agent.withMiddleware( {
    beforeToolCall: ( ctx ) => {
        if ( ctx.tool?.getName() == "dangerousTool" ) {
            return AiMiddlewareResult.cancel( "This tool is not allowed." )
        }
        return AiMiddlewareResult.continue()
    },

    wrapLLMCall: ( ctx, handler ) => {
        var start  = getTickCount()
        var result = handler()
        metricsService.record( "llm.latency", getTickCount() - start )
        return result
    },

    onError: ( ctx ) => {
        alertService.notify( "Agent error in #ctx.phase#: #ctx.error.message#" )
        return AiMiddlewareResult.continue()
    }
} )

Structs are automatically wrapped in StructMiddlewareAdapter — you only define the hooks you need.

Class-based — reusable, configurable, independently testable:

import bxModules.bxai.models.middleware.BaseAiMiddleware;
import bxModules.bxai.models.middleware.AiMiddlewareResult;

class extends="BaseAiMiddleware" {

    property name="tenantId" type="string";

    function init( required string tenantId ) {
        variables.tenantId = arguments.tenantId
        variables.name = "Tenant Audit Middleware"
        variables.description = "Logs all AI tool calls to the tenant audit trail"
        return this
    }

    AiMiddlewareResult function beforeToolCall( required struct context ) {
        auditLog.record(
            tenantId : variables.tenantId,
            tool     : context.tool?.getName() ?: "unknown",
            args     : context.toolCall?.function?.arguments ?: "{}"
        )
        return AiMiddlewareResult.continue()
    }

}

🚀 Composing Middleware

Middleware stacks compose cleanly — just pass an array:

agent = aiAgent(
    name       : "production-agent",
    middleware : [
        new LoggingMiddleware( logToConsole: false ),
        new RetryMiddleware( maxRetries: 3 ),
        new GuardrailMiddleware( blockedTools: [ "deleteRecord" ] ),
        new MaxToolCallsMiddleware( maxCalls: 15 ),
        new HumanInTheLoopMiddleware( toolsRequiringApproval: [ "placeOrder" ] )
    ]
)

Or fluently, one at a time:

agent
    .withMiddleware( new LoggingMiddleware() )
    .withMiddleware( new RetryMiddleware( maxRetries: 3 ) )
    .withMiddleware( new GuardrailMiddleware( blockedTools: [ "deleteRecord" ] ) )

In production, logging + retry + guardrails is the baseline stack. Add MaxToolCallsMiddleware for complex reasoning agents. Add HumanInTheLoopMiddleware for any agent touching money, data, or external systems. Use FlightRecorderMiddleware in record mode during QA and replay mode in CI.

What's Next

In Part 5, we close the series with a deep dive into BoxLang AI's provider architecture — how the capability system works, how BaseService and OpenAIService are structured, how to add custom providers, and a tour of the full 17-provider ecosystem.

📖 Full Documentation 📦Install Today: install-bx-module bx-ai 🫶Professional Support

← Previous

Next ->

The post BoxLang AI Deep Dive — Part 4 of 7: Middleware — The Missing Layer in Every AI Framework 🧵 appeared first on foojay.

]]>
https://foojay.io/today/boxlang-ai-deep-dive-part-4-of-7-middleware-the-missing-layer-in-every-ai-framework-%f0%9f%a7%b5/feed/ 0