BoxLang ๐ A New JVM Dynamic Language Learn More...
|:------------------------------------------------------: |
| โก๏ธ B o x L a n g โก๏ธ
| Dynamic : Modular : Productive
|:------------------------------------------------------: |
Copyright Since 2023 by Ortus Solutions, Corp
ai.boxlang.io | www.boxlang.io
ai.ortussolutions.com | www.ortussolutions.com
ย
Welcome to the BoxLang AI Module ๐ The official AI library for BoxLang that provides a unified, fluent API to orchestrate multi-model workflows, autonomous agents, RAG pipelines, and AI-powered applications. One API โ Unlimited AI Power! โจ
BoxLang AI eliminates vendor lock-in and simplifies AI integration by providing a single, consistent interface across 16+ AI providers. Whether you're using OpenAI, Claude, Gemini, Grok, DeepSeek, MiniMax, Ollama, or Perplexityโyour code stays the same. Switch providers, combine models, and orchestrate complex workflows with simple configuration changes. ๐
runAsync() on every runnable; aiParallel()
for concurrent parallel pipelinesBoxLang is open source and licensed under the Apache 2 license.
๐ You can also get a professionally supported version with enterprise features and support via our BoxLang +/++ Plans (www.boxlang.io/plans). This includes more vector memories, enhanced features, Agent Dashboard and much more.
You can use BoxLang AI in both operating system applications, AWS Lambda, and web applications. For OS applications, you can use the module installer to install the module globally. For AWS Lambda and web applications, you can use the module installer to install it locally in your project or CommandBox as the package manager, which is our preferred method for web applications.
๐ New to AI concepts? Check out our Key Concepts Guide for terminology and fundamentals, or browse our FAQ for quick answers to common questions. We also have a Quick Start Guide and our intense AI BootCamp available to you as well.
You can easily get started with BoxLang AI by using the module installer for building operating system applications:
install-bx-module bx-ai
This will install the latest version of the BoxLang AI module in your
BoxLang environment. Once installed, configure your default AI
provider and API key in boxlang.json (https://boxlang.ortusbooks.com/getting-started/configuration):
{
"modules": {
"bxai": {
"settings": {
"provider": "openai",
"apiKey": "${OPENAI_API_KEY}"
}
}
}
}
๐ก Tip: Use environment variable placeholders like
${OPENAI_API_KEY}so you never commit secrets to source control. Each provider also auto-detects its own env var according to its name(e.g.OPENAI_API_KEY,CLAUDE_API_KEY,GEMINI_API_KEY).
Below is the full reference of every setting you can place under
settings in boxlang.json:
{
"modules": {
"bxai": {
"settings": {
"provider": "openai",
"apiKey": "${OPENAI_API_KEY}",
"defaultParams": {
"model": "gpt-4o",
"temperature": 0.7,
"max_tokens": 2000
},
"memory": {
"provider": "window",
"config": {
"maxMessages": 20
}
},
"providers": {
"openai": {
"params": { "model": "gpt-4o", "temperature": 0.7 },
"options": { "timeout": 60 }
},
"claude": {
"params": { "model": "claude-3-5-sonnet-20241022" }
},
"ollama": {
"params": { "model": "qwen3:0.6b" }
}
},
"timeout": 45,
"logRequest": false,
"logRequestToConsole": false,
"logResponse": false,
"logResponseToConsole": false,
"returnFormat": "single",
"skillsDirectory": "/.ai/skills",
"autoLoadSkills": true,
"globalSkills": []
}
}
}
}
| Setting | Type | Default | Description |
|---|---|---|---|
provider
| string
| "openai"
| Default AI provider to use for all requests |
apiKey
| string
| ""
| Default API key; each provider also reads its own env var
(e.g. OPENAI_API_KEY) |
defaultParams
| struct
| {}
| Default request parameters sent to every provider (e.g.
model, temperature, max_tokens) |
memory.provider
| string
| "window"
| Default memory type: window,
cache, file, session,
summary, jdbc, hybrid, or
any vector provider |
memory.config
| struct
| {}
| Provider-specific memory configuration (e.g.
maxMessages, cacheName) |
providers
| struct
| {}
| Per-provider overrides โ keys are provider names, values
have params and options structs |
timeout
| numeric
| 45
| Default HTTP request timeout in seconds |
logRequest
| boolean
| false
| Log outgoing AI requests to ai.log
|
logRequestToConsole
| boolean
| false
| Print outgoing AI requests to the console (useful for debugging) |
logResponse
| boolean
| false
| Log AI responses to ai.log
|
logResponseToConsole
| boolean
| false
| Print AI responses to the console (useful for debugging) |
returnFormat
| string
| "single"
| Default response format: single,
all, raw, json,
xml, or structuredOutput
|
skillsDirectory
| string
| "/.ai/skills"
| Directory scanned for SKILL.md files at
startup. Set to "" to disable auto-discovery |
autoLoadSkills
| boolean
| true
| When true, skills found in
skillsDirectory are auto-loaded and injected into
every aiAgent() as global skills |
globalSkills
| array
| []
| Internal โ populated at startup with auto-discovered
skills; access via aiGlobalSkills()
|
After that you can leverage the global functions (BIFs) in your BoxLang code. Here is a simple example:
// chat.bxs
answer = aiChat( "How amazing is BoxLang?" )
println( answer )
You can then run your BoxLang script like this:
boxlang chat.bxs
In order to build AWS Lambda functions with Boxlang AI for serverless
AI agents and applications, you can use the Boxlang
AWS Runtime and our AWS
Lambda Starter Template. You will use the
install-bx-module as well to install the module locally
using the --local flag in the resources
folder of your project:
cd src/resources
install-bx-module bx-ai --local
Or you can use CommandBox as well and store your dependencies in the
box.json descriptor.
box install bx-ai resources/modules/
To use BoxLang AI in your web applications, you can use CommandBox as the package manager to install the module locally in your project. You can do this by running the following command in your project root:
box install bx-ai
Just make sure you have already a server setup with BoxLang. You can check our Getting Started with BoxLang Web Applications guide for more details on how to get started with BoxLang web applications.
The following are the AI providers supported by this module. Please note that in order to interact with these providers you will need to have an account with them and an API key. ๐
Here is a matrix of the providers and their feature support. Please keep checking as we will be adding more providers and features to this module. ๐
| Provider | Chat & Streaming | Real-time Tools | Embeddings | TTS (Speech) | STT (Transcription) |
|---|---|---|---|---|---|
| AWS Bedrock | โ | โ | โ | โ | โ |
| Claude | โ | โ | โ | โ | โ |
| Cohere | โ | โ | โ | โ | โ |
| DeepSeek | โ | โ | โ | โ | โ |
| Docker Model Runner | โ | โ | โ | โ | โ |
| ElevenLabs | โ | โ | โ | โ (Premium) | โ (Scribe v1) |
| Gemini | โ | [Coming Soon] | โ | โ | โ |
| Grok | โ | โ | โ | โ | โ |
| Groq | โ | โ | โ | โ | โ (Whisper) |
| HuggingFace | โ | โ | โ | โ | โ |
| Mistral | โ | โ | โ | โ (Voxtral) | โ (Voxtral) |
| MiniMax | โ | โ | โ | โ | โ |
| Ollama | โ | โ | โ | โ | โ |
| OpenAI | โ | โ | โ | โ | โ (Whisper) |
| OpenAI-Compatible | โ | โ | โ | โ | โ |
| OpenRouter | โ | โ | โ | โ | โ |
| Perplexity | โ | โ | โ | โ | โ |
| Voyage | โ | โ | โ (Specialized) | โ | โ |
Every provider exposes a runtime capability API so you can introspect what it supports without consulting documentation โ and without risking cryptic errors when you call an unsupported operation. ๐ก๏ธ
// Get all capabilities a provider supports
var provider = aiService( "openai" );
var caps = provider.getCapabilities();
// โ [ "chat", "stream", "embeddings" ]
// Check a specific capability before using it
if ( provider.hasCapability( "embeddings" ) ) {
var embedding = aiEmbed( "Hello world" );
}
// Voyage is embeddings-only โ getCapabilities() reflects this
var voyage = aiService( "voyage" );
voyage.getCapabilities(); // โ [ "embeddings" ]
voyage.hasCapability( "chat" ); // โ false
The built-in BIFs (aiChat, aiChatStream,
aiEmbed) automatically use this system and throw a clear
UnsupportedCapability exception when the selected
provider does not implement the required capability:
// This will throw UnsupportedCapability โ Voyage has no chat capability
aiChat( "Hello?", provider: "voyage" );
// This will throw UnsupportedCapability โ Claude has no embeddings capability
aiEmbed( "some text", provider: "claude" );
Capabilities map to the following capability
interfaces (in models/providers/capabilities/):
| Capability String | Interface | Methods Provided |
|---|---|---|
chat, stream
| IAiChatService
| chat(), chatStream()
|
embeddings
| IAiEmbeddingsService
| embeddings()
|
speech
| IAiSpeechService
| speak()
|
transcription
| IAiTranscriptionService
| transcribe(), translate()
|
Here's a taste of what you can do with BoxLang AI. For full details, explore our complete documentation.
// Simple chat โ auto-detects OPENAI_API_KEY
answer = aiChat( "What is BoxLang?" )
// Use a specific provider and model
answer = aiChat(
"Explain quantum computing",
params : { model: "claude-3-5-sonnet-20241022" },
options: { provider: "claude" }
)
// Stream responses in real-time
aiChatStream(
"Write a poem about coding",
( chunk ) => print( chunk )
)
// Create an agent with tools and memory
var agent = aiAgent(
name : "researcher",
description : "Research assistant with web search",
instructions: "Always cite your sources",
tools : [
aiTool( "search", "Search the web", { query: "string" }, searchWeb )
],
memory : aiMemory( "window", { maxMessages: 10 } )
)
var result = agent.run( "What are the latest trends in AI?" )
๐ AI Agents Guide ยท Tools & Function Calling
// Load documents into vector memory for semantic search
var loader = aiDocuments( "pdf", "./docs/*.pdf" )
.chunk( 1000, 200 )
var memory = aiMemory( "box", { collection: "knowledge-base" } )
loader.ingest( memory )
// Query with context retrieval
var relevant = memory.getRelevant( "How do I configure BoxLang?", 5 )
When the bx-spreadsheet module is installed, you can use
its SpreadsheetLoader for BoxLang AI document loading workflows:
SpreadsheetLoader in
src/main/bx/loaders/SpreadsheetLoader.bx for BoxLang AI
document loading workflowsDocument objectsrowsAsDocuments)hasHeaders) and
sheet filtering (sheets)IDocumentLoader contract via BaseDocumentLoader
import bxModules.bxSpreadsheet.loaders.SpreadsheetLoader;
// One document per sheet (default)
var docs = new SpreadsheetLoader( source: "./data/customers.xlsx" ).load();
// One document per row on a specific sheet
var rowDocs = new SpreadsheetLoader( source: "./data/customers.xlsx" )
.rowsAsDocuments()
.sheets( [ "Customers" ] )
.load();
๐ Memory Systems ยท Vector Memory & RAG ยท Document Loaders
// Chain models, transformers, and custom logic
var pipeline = aiModel( "openai" )
.to( aiTransform( "json", { stripMarkdown: true } ) )
.to( aiTransform( (data) => data.users ) )
var users = pipeline.run( "Generate a JSON array of 5 users with name and email" )
๐ AI Pipelines
// Create an MCP server exposing custom tools
var mcpSrv = mcpServer( "my-tools", "Business tools API" )
.registerTool( aiTool( "getCustomer", "Fetch customer by ID", { id: "string" }, fetchCustomer ) )
.enableCORS( ["*"] )
๐ MCP Protocol Guide
// Text-to-speech
response = aiSpeak( "Welcome to BoxLang!", params: { voice: "nova" } )
response.saveToFile( "./welcome.mp3" )
// Speech-to-text
text = aiTranscribe( "./recording.mp3" )
๐ Speech Synthesis ยท Transcription
// Load skills from a directory for agent behavior
var skills = aiSkill( "./skills", recurse: true )
var agent = aiAgent( name: "coder", skills: skills )
๐ AI Skills Guide
// Non-blocking chat
var future = aiChatAsync( "Analyze this data..." )
var result = future.get()
// Run multiple pipelines in parallel
var results = aiParallel({
summary : aiModel( "openai" ),
tags : aiModel( "claude" ),
tone : aiModel( "gemini" )
}).runAsync( "Review this article" ).get()
๐ Async Operations
| Function | Purpose | Parameters | Return Type | Async Support |
|---|---|---|---|---|
aiAgent()
| Create autonomous AI agent | name,
description, instructions,
model, memory, tools,
subAgents, params,
options, mcpServers=[],
skills=[], availableSkills=[]
| AiAgent Object (supports runAsync()) | โ |
aiChat()
| Chat with AI provider | messages,
params={}, options={}
| String/Array/Struct | โ |
aiChatAsync()
| Async chat with AI provider | messages, params={}, options={}
| BoxLang Future | โ |
aiChatRequest()
| Compose a reusable chat request object (useful for advanced pipelines and middleware) | messages, params,
options, headers
| AiChatRequest Object | N/A |
aiChatStream()
| Stream chat responses from AI provider | messages, callback,
params={}, options={}
| void | N/A |
aiChunk()
| Split text into chunks for RAG ingestion or token-window management | text, options={}
(chunkSize, overlap, strategy)
| Array of Strings | N/A |
aiDocuments()
| Create fluent document loader | source, config={}
| IDocumentLoader Object | N/A |
aiEmbed()
| Generate embeddings | input,
params={}, options={}
| Array/Struct | N/A |
aiMemory()
| Create memory instance | memory,
key, userId,
conversationId, config={}
| IAiMemory Object | N/A |
aiMessage()
| Build message object | message
| ChatMessage Object | N/A |
aiModel()
| Create AI model wrapper | provider,
apiKey, tools,
mcpServers=[], skills=[]
| AiModel Object | N/A |
aiPopulate()
| Populate class/struct from JSON | target, data
| Populated Object | N/A |
aiService()
| Create AI service provider | provider, apiKey
| IService Object | N/A |
aiSkill()
| Create or discover AI skills | path,
name, description,
content, recurse=true
| AiSkill / Array | N/A |
aiGlobalSkills()
| Get the globally shared skill pool | (none) | Array of AiSkill | N/A |
aiSpeak()
| Convert text to speech (TTS) | text,
params={}, options={}
| AiSpeechResponse / File path | N/A |
aiTokens()
| Estimate token count for a text string | text, options={}
(method: characters|words)
| Numeric | N/A |
aiTool()
| Create tool for real-time processing | name, description, callable
| Tool Object | N/A |
aiToolRegistry()
| Get the singleton AI Tool Registry | (none) | AIToolRegistry Object | N/A |
aiTranscribe()
| Transcribe audio to text (STT) | audio, params={}, options={}
| String / AiTranscriptionResponse | N/A |
aiTranslate()
| Translate non-English audio to English | audio, params={}, options={}
| String / AiTranscriptionResponse | N/A |
aiParallel()
| Run multiple named runnables concurrently and collect results | runnables (struct of { name:
IAiRunnable }) | AiRunnableParallel Object | โ
(via runAsync()) |
aiTransform()
| Create data transformer | transformer, config={}
| Transformer Runnable | N/A |
MCP()
| Create MCP client for Model Context Protocol servers | baseURL
| MCPClient Object | N/A |
mcpServer()
| Get or create MCP server for exposing tools | name="default",
description, version,
cors, statsEnabled, force
| MCPServer Object | N/A |
aiWebSearch()
| Search the web via a pluggable provider | query, params={}, options={}
(provider, maxResults)
| Array of {title, url, snippet}
| โ |
aiWebSearchAsync()
| Search the web asynchronously | query,
params={}, options={}
(provider, maxResults)
| BoxLang Future | โ |
Note on Return Formats: When using pipelines (runnable chains), the default return format is
raw(full API response), giving you access to all metadata. Use.singleMessage(),.allMessages(), or.withFormat()to extract specific data. TheaiChat()BIF defaults tosingleformat (content string) for convenience. See the Pipeline Return Formats documentation for details.
Visit the GitHub repository for release notes. You can also file a bug report or improvement suggestion via GitHub Issues.
Follow these instructions if you want to contribute to the project:
To build and test the module locally, you'll need BoxLang and Gradle installed.
# Clone the repository
git clone https://github.com/ortus-solutions/bx-ai.git
cd bx-ai
# Restore agent skills from skills-lock.json
npx skills experimental_install
# Download BoxLang language files for compilation
./gradlew downloadboxLang
# Build the module (outputs to build/module/)
./gradlew build
# Skip tests for faster builds during development
./gradlew shadowJar -x test
# Run all tests
./gradlew test
# Run a specific test class
./gradlew test --tests "ortus.boxlang.ai.bifs.aiChatTest"
# Start Ollama for local testing (requires Docker)
docker compose up -d ollama
curl http://localhost:11434/api/tags # Verify model availability
After building, the compiled module is available in
build/module/ and can be loaded by any BoxLang application.
BoxLang is a professional open-source project and it is completely funded by the community and Ortus Solutions, Corp. Ortus Patreons get many benefits like a cfcasts account, a FORGEBOX Pro account and so much more. If you are interested in becoming a sponsor, please visit our patronage page: https://patreon.com/ortussolutions
"I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
response_format, so structured output was silently unsupported โ the schema was never sent and populateStructuredOutput() failed parsing the model's prose. Fixed by injecting a synthetic structured_output tool (requested schema as input_schema), pinning tool_choice to it, and routing the returned tool_use.input through populateStructuredOutput(). Now fails loud with StructuredOutputError + ai-log when the forced tool block is absent (max_tokens truncation / refusal / tool_choice not honored) instead of feeding prose into the JSON populator. Adds deterministic, credential-free tests (beforeLLMCall packet capture + wrapLLMCall canned-response extraction/throw) for both providers, plus a live Bedrock structured-output test. #198@AITool scan generates wrong parameter schema: aiToolRegistry().scanClass() was wrapping annotated methods in a generic (args) => lambda. getArgumentsSchema() introspected that wrapper and produced a single args property instead of the actual method parameters (e.g., orderId), and the required array was always empty for scanned tools. Fixed by storing the original method's parameter metadata (name, type, required) on the ClosureTool via setMethodParameters() and using it during schema generation when present. The wrapper lambda was also corrected to forward named arguments via the arguments scope, ensuring invocation works correctly once the schema exposes real parameter names.formatToolsForClaude() converts OpenAI-compatible tool schemas to the Claude-native input_schema format.executeBedrockTool() processes Claude tool_use blocks, invokes the registered tool, and appends tool_result messages back into the conversation.tool_use/tool_result blocks) without flattening them via toString().BedrockTest.java covering tool-enabled chat and multi-turn tool interactions.mcpServer parameter in handleCORSPreflight() to targetServer to avoid a case-insensitive name collision with the MCPServer import, which caused the stricter BoxLang compiler to reject the file and 500 every MCP request.DataProcessingScheduler.bx, ReportingScheduler.bx).Web Search Tools & BIF: New aiWebSearch() BIF and WebSearchTools class providing multi-provider web search for AI agents.
aiWebSearch(query, params, options) BIF โ simple entry point for web search (renamed from webSearch()).webSearch@bxai tool โ auto-registered AI tool enabling agents to search the web during conversations.aiWebSearchAsync(query, options) BIF โ non-blocking variant returning a BoxFuture resolved on the io-tasks executor (renamed from webSearchAsync()); all providers also expose searchAsync() directly.searchAsync(query, options) โ all search providers now expose a non-blocking async variant that returns a BoxFuture resolved on the io-tasks executor.BoxRegisterInterceptor():
beforeAIWebSearch โ fired before any search executes (provider, query, options)afterAIWebSearch โ fired after search completes (results + cached: boolean flag for future caching support)onAIWebSearchRequest โ fired immediately before the HTTP/API request is sent (url, method, headers)onAIWebSearchResponse โ fired after a successful HTTP/API response is received (statusCode, response)onAIWebSearchError โ fired on any search failure before the exception propagates (error)IWebSearch):
BRAVE_API_KEY env varGOOGLE_API_KEY + GOOGLE_SEARCH_ENGINE_IDTAVILY_API_KEY env varEXA_API_KEY env var; supports type: keyword|neural|magic, country, and language filters[{title, url, snippet, publishedDate, domain, score, thumbnail, language}] regardless of underlying API.webSearch section for global configuration (default provider, max results, timeout, API keys including exaApiKey, logging).BaseSearch for consistent logging, error handling, and proxy support.MCP Server IP Allowlist & Proxy-Aware Client IP Extraction: MCPServer now supports IP-based access control with automatic client IP resolution from common proxy headers.
withAllowedIPs(ips): Configure allowed IP addresses or CIDR ranges. Pass empty array to allow all (default).addAllowedIP(ip) / clearAllowedIPs(): Incremental allowlist management.hasAllowedIPs(): Check if IP filtering is active.verifyClientIP(clientIP, requestData): Validate a client IP against the allowlist with exact match and CIDR range support.getClientIP(requestData): Extract client IP from trusted proxy headers (X-Forwarded-For, CF-Connecting-IP, True-Client-IP, X-Real-IP) with fallback to cgi.REMOTE_ADDR for direct connections.192.168.1.100) and CIDR blocks (192.168.0.0/24) for IPv4 and IPv6.MCPServerStats.security.ipFilterFailures counter and exposed in getStats() / getSummary().INVALID_REQUEST JSON-RPC error code.Fluent Builder API for Audio BIFs: aiSpeak(), aiTranscribe(), and aiTranslate() now
support a fluent builder API. Calling any of these BIFs with no arguments returns the request
object for chaining.
AiSpeechRequest gains:
of(text) static factory.text().model().provider().apiKey().voice().speed().instructions().outputFile().outputFormat().timeout().male(), .female()).asMP3(), .asWav(), .asFlac(), .asOpus(), .asPCM()).withParams().withOptions().withLogging().speak() terminatorAiTranscriptionRequest gains:
of(audio) static factory.file(path).url(url).data(binary).model().provider().apiKey().language().inputFormat().timeout().withWordTimestamps(), .withSegmentTimestamps(), .withTimestamps()).diarize().asJSON(), .asText(), .asVerboseJSON(), .asSRT(), .asVTT()).withParams().withOptions().withLogging().transcribe() and .translate()Image Generation โ aiImage(): New BIF for generating images from text prompts using any provider that implements IAiImageService.
aiImage( prompt, params, options ) BIF: Generate one or more images from a text description. Returns an AiImageResponse (with hasImages(), getCount(), getFirstURL(), getFirstBase64(), getRevisedPrompt(), saveToFile(), saveAllToDirectory(), toDataURI(), getMimeType(), toStruct()) or saves directly to a file via options.outputFile.IAiImageService interface: New capability interface implemented by providers that support text-to-image generation (generateImage()).AiImageRequest object: Carries prompt, n, size, quality, style, instructions, outputFormat, and outputFile. All fields fluent via BoxLang property conventions.AiImageResponse object: Wraps one or more generated images, each as a struct with url, data (binary), mimeType, and revisedPrompt. Convenience methods for saving, encoding, and embedding as data URIs.gpt-image-1 (default) and DALL-E models via /v1/images/generations. Supports quality/style/size controls and format/compression parameters.imagen-3.0-generate-008) via the Gemini API predict endpoint. Returns binary image data directly; size maps to aspect ratio (1:1, 16:9, 9:16).grok-2-image via https://api.x.ai/v1/images/generations (OpenAI-compatible format).https://openrouter.ai/api/v1/images/generations (OpenAI-compatible format).beforeAIImageGeneration, afterAIImageGeneration, onAIImageRequest, onAIImageResponse.image settings block in module config: defaultProvider, defaultApiKey, defaultModel, defaultSize, defaultQuality, defaultStyle, defaultInstructions.generateImage@bxai agent tool: New ImageTools class (models/tools/image/ImageTools.bx) auto-registered in the global tool registry at module startup. Generates an image from a text prompt, saves to a file (auto-generates a temp file when no outputFile is supplied), and returns the absolute path. Opt-in: aiAgent( tools: [ "generateImage@bxai" ] ).MCP Server Observability & Analytics Improvements
byMethod, byTool, byUri, byName, and byCode counters in MCPServerStats were plain struct mutations happening outside any lock, causing silent lost updates under concurrent load. All are now wrapped in dedicated named locks.AtomicInteger counters (security.authFailures, security.apiKeyFailures, security.bodySizeViolations) visible in getStats() and getSummary(). MCPServer exposes a recordSecurityFailure(type) method for processor delegation.SERVER_PAUSED are now recorded in stats (previously they were silently dropped from all counters).onMCPError for METHOD_NOT_FOUND: The default: switch case was the only error path that never fired the onMCPError interception point. Fixed.handleToolCall() now records a tool error via recordToolError() before rethrowing any exception. MCPServerStats gains byTool[name].errors and an errors.byTool roll-up counter.MCPServerStats gains an activeRequests AtomicInteger; handleRequest() increments it on entry and decrements it in a finally block. Exposed in getStats() and getSummary().getSummary() now includes requestsPerMinute calculated from uptime and total request count.HTTPTransport reads the X-Request-ID request header (or generates a UUID if absent); StdioTransport always generates one. The ID is echoed as X-Request-ID in the response headers and included in onMCPRequest and onMCPResponse event payloads.Agent Registry
โ New AIAgentRegistry singleton (access via aiAgentRegistry() BIF) modeled after AIToolRegistry. Allows users to explicitly register AiAgent instances for centralized discoverability, observability, and analytics.
aiAgentRegistry().register( agent, module ) โ register an AiAgent instance with optional module namespace. Key convention: agentName or agentName@moduleName.aiAgentRegistry().unregister( key ) / unregisterByModule( module ) โ remove agents from the registry.aiAgentRegistry().resolveAgents( array ) โ lazily resolve a mixed array of string keys and AiAgent instances into AiAgent[].aiAgentRegistry().listAgents() โ returns a struct of all registered agents mapped to { name, description, module } for analytics dashboards and introspection.aiAgentRegistry().getAgentInfo( key ) โ returns { name, description, module } for a single registry key.onAIAgentRegistryRegister, onAIAgentRegistryUnregister โ fired on every register/unregister operation for external observability hooks.aiAgent() BIF gains two new parameters: register: false (opt-in flag) and module: "" โ when register: true the agent is automatically placed in the registry at creation time. Defaults to false to prevent memory leaks from sub-agents and throwaway agents.MCP Client Stats & Observability
MCPClient now tracks internal usage and performance metrics via a new MCPClientStats instance (using atomic variables for thread safety).getStats() โ returns a fully serializable struct with call totals, per-operation-type breakdowns, response time avg/min/max, per-tool invocation stats (count, totalTime, avgTime), per-URI resource counts, per-name prompt counts, and error tracking.getSummary() โ lightweight summary with totalCalls, successRate, avgResponseTime, per-type totals, totalErrors, and lastCallAt.resetStats() โ resets all counters to zero (fluent).onMCPClientRequest โ fires before the HTTP request with { client, baseURL, operation, name, requestBody }.onMCPClientResponse โ fires on success with { client, baseURL, operation, name, response, executionTime, statusCode }.onMCPClientError โ fires on HTTP errors (bad status / JSON-RPC error) and on network-level exceptions with { client, baseURL, operation, name, error, statusCode, executionTime } (includes exception key when fired from a catch block).tool (covers listTools + send), resource (covers listResources + readResource), prompt (covers listPrompts + getPrompt), discovery (getCapabilities).MCP Server Pause/Resume
MCPServer now supports pausing and resuming via pause() and resume() fluent methods. While paused, the server remains registered in the global registry but rejects all incoming JSON-RPC requests (except ping) with a SERVER_PAUSED error (code -32005). This lets an admin interface or AI service temporarily halt a server without destroying its configuration, tools, resources, or prompts. Resume restores normal request handling instantly.pause() โ pause the server; fires onMCPServerPause interception point.resume() โ resume the server; fires onMCPServerResume interception point.isPaused() โ returns true if currently paused.getSummary() now includes a paused boolean field.SERVER_PAUSED: -32005 error code added to RPC_ERROR_CODES.onMCPServerPause, onMCPServerResume.agent.buildSystemMessage() for debugging and inspection.systemMessage propertyClosureTool.getArgumentsSchema() now maps BoxLang parameter types to their correct JSON Schema types instead of hard-coding everything as "string". numeric/integer/float/double โ "number", boolean โ "boolean", array โ "array" (with "items": {}), struct โ "object". Untyped params default to "string". This means the AI receives accurate type hints and sends native JSON types (booleans, numbers, arrays, objects) instead of string-encoded values.ClosureTool.doInvoke(): MCP clients that send JSON fields as real objects/arrays (instead of pre-stringified JSON) caused a "Can't cast Struct to a string" error before the callable ran. The fix walks the callable's declared parameters and jsonSerialize()s any non-simple value whose declared type is string, keeping the schema contract intact while accepting both wire formats. Callables that declare struct, array, or any parameters are left untouched.Audio Support โ Text-to-Speech, Transcription, and Translation:
aiSpeak( text, params, options ) BIF: Convert text to speech using any provider that supports TTS. Returns an AiSpeechResponse (with hasAudio(), saveToFile(), getBase64(), getMimeType(), getSize()) or saves directly to a file via options.outputFile.aiTranscribe( audio, params, options ) BIF: Transcribe audio (file path, URL, or binary) to text. Returns the transcript string by default or a full AiTranscriptionResponse when options.returnFormat = "response".aiTranslate( audio, params, options ) BIF: Translate non-English audio to English text using supported providers.IAiSpeechService interface: Implemented by providers that support TTS (speak()).IAiTranscriptionService interface: Implemented by providers that support STT (transcribe() + translate()).ElevenLabsService: New provider supporting high-quality TTS via eleven_multilingual_v2 and STT via scribe_v1. Use aiService("elevenlabs", apiKey).beforeAISpeech, afterAISpeech, beforeAITranscription, afterAITranscription, beforeAITranslation, afterAITranslation.audio settings block in module config: defaultVoice, defaultOutputFormat, defaultSpeechModel, defaultTranscriptionModel.Audio Agent Tools โ speak@bxai, transcribe@bxai, translate@bxai: New AudioTools class (models/tools/audio/AudioTools.bx) auto-registered in the global tool registry at module startup. speak@bxai converts text to speech and returns the saved file path (auto-generates a temp file when no outputFile is supplied). transcribe@bxai transcribes a local file or URL to plain text. translate@bxai translates any-language audio to English text. Opt-in by name: aiAgent( tools: [ "speak@bxai", "transcribe@bxai", "translate@bxai" ] ).
FileSystem Agent Tools โ New FileSystemTools class (models/tools/filesystem/FileSystemTools.bx) with 19 @AITool-annotated methods covering the full filesystem lifecycle. NOT auto-registered โ opt-in only via aiToolRegistry().scanClass() so agents never get filesystem access unless explicitly granted. Supports a path-guard constructor (allowedPaths: [...]) that canonicalizes and validates every path argument before execution, blocking directory-traversal attacks. Tool keys: readFile@bxai, readMultipleFiles@bxai, writeFile@bxai, appendFile@bxai, editFile@bxai, fileMetadata@bxai, pathExists@bxai, deleteFile@bxai, moveFile@bxai, copyFile@bxai, searchFiles@bxai, listAllowedDirectories@bxai, listDirectory@bxai, directoryTree@bxai, createDirectory@bxai, deleteDirectory@bxai, zipFiles@bxai, unzipFile@bxai, checkZipFile@bxai.
Async Runnables and Parallel Execution:
runAsync() on all runnables (IAiRunnable, AiBaseRunnable): Every runnable now has a non-blocking runAsync(input, params, options) method that dispatches execution to the io-tasks virtual thread pool and returns a BoxFuture. Mirrors the existing aiChatAsync, loadAsync(), and seedAsync() patterns throughout the module.AiRunnableParallel class (models/runnables/AiRunnableParallel.bx): New runnable that accepts a named struct of runnables, fans them out concurrently via runAsync(), and returns a { name: result } struct once all futures complete. Mirrors LangChain's RunnableParallel โ a structural parallel composition primitive that integrates cleanly into the existing pipeline system via .to(), .run(), and .runAsync().aiParallel() BIF: Creates an AiRunnableParallel from a named struct of runnables. aiParallel({ summary: summaryAgent, analysis: analysisAgent }).run("document") runs both concurrently and returns { summary: "...", analysis: "..." }.chatStream() across all providers never fires the onAITokenCount event, making streaming calls completely invisible to usage tracking, billing, and monitoring. The non-streaming chat() path fires it correctly.AiModel.stream(): inject agent and model middleware into chatRequest, matching the existing pattern in run()DockerModelRunnerService: capture arguments into local vars before retryOnModelLoading closure to prevent ArgumentsScope resolution failureOpenAIService.chat(): capture chatRequest before nested .each() closures for tool callingOpenAIService.chatStream(): scope callback and chatRequest for sendStreamRequest call and tool-calling .each() closureCohereService.chat(): capture chatRequest before .map() tool closureClaudeService, GeminiService, CohereService, and BedrockService chat() methods called sendChatRequest() / sendBedrockRequest() directly, silently bypassing the entire wrapLLMCall middleware chain. beforeLLMCall, wrapLLMCall, and afterLLMCall hooks (including FlightRecorderMiddleware, retry wrappers, and any custom LLM wrappers) never fired for these providers.onAITokenCount event and add missing event on the following services: BedrockService, ClaudeService, CohereService, GeminiServicescan() and scanClass() where not working accordingly with all cases and permutations.aiAgent() bif, skills, availableSkills can now be an array or a single skill, we will normalize it to an array internally. This allows for more flexible agent construction with a single skill without needing to wrap it in an array.ModuleConfig.bx listens now to onRuntimeStart() in order to setup skills and more, so caches and other things are properly loaded before the modules._input System Variable: Auto-inject previous stage output into message templates via ${_input}. For struct outputs, individual fields are flattened as ${_input_fieldName} for template access. Enables clean, composable multi-stage AI pipelines without manual transformation steps.aiTransform() needd to process instances of AiTransformRunnable and BaseTransformer classes, allowing for more flexible and reusable transformation logic.config on all BaseTransformer classes was missing.aiTransform() BIF was called with a non-string or closure, the throw() was invalid.aiSkill() BIF + withSkills() / withAvailableSkills() APIs on AiModel and AiAgent): Composable, reusable knowledge blocks โ following the Claude Agent Skills open standard โ that can be injected into any model or agent system message at runtime.
aiSkill( path | name, description, content, recurse ) โ Creates or discovers AiSkill instances. Pass a file path to load a single SKILL.md, a directory path to auto-discover all skills recursively, or name/description/content for inline definitions with no files needed.aiGlobalSkills() โ Returns the globally shared pool of skills auto-injected into every new agent's availableSkills pool. Populated via ModuleConfig.bx โ settings.globalSkills.withSkills() / addSkill()): Full skill content is injected into the system message on every call. Best for small, universally relevant guidance.withAvailableSkills() / addAvailableSkill()): Only a compact index (name + description) is included in the system message. The LLM calls the auto-registered loadSkill( name ) tool to fetch full content on demand. Best for large or rarely needed skill libraries.activateSkill( name ) โ Moves a skill from the lazy pool to always-on, promoting it for the rest of the session.buildSkillsContent() โ Renders the combined skills system-message block for inspection or custom injection..ai/skills/. The file is Markdown with an optional YAML frontmatter block containing description. The body is the instruction content. If frontmatter is absent, the first paragraph of body text is used as the description.AiModel and AiAgent getConfig() now include activeSkillCount, availableSkillCount, and skills (a struct with activeSkills and availableSkills name/description arrays) for full introspection.aiAgent() BIF gains skills: [] and availableSkills: [] construction-time parameters. Global skills from aiGlobalSkills() are automatically prepended to every new agent's available pool.aiModel() BIF gains a skills: [] construction-time parameter.listTools() and registered as MCPTool instances โ no manual Tool construction required.
MCPTool class (models/tools/MCPTool.bx) implements ITool by proxying a single MCP server tool. It converts the MCP inputSchema to the OpenAI function-calling schema format and forwards invocations to the server via MCPClient.send().withMCPServer( server, config ) fluent method on AiAgent and AiModel. Accepts a URL string or a pre-configured MCPClient instance. Optional config struct supports token, timeout, headers, user, and password.withMCPServers( servers ) fluent method on AiAgent and AiModel for seeding from multiple servers in one call. Each entry can be a URL string, a config struct { url, token, timeout, โฆ }, or a pre-configured MCPClient.listMcpServers() method on AiAgent and AiModel returns the list of currently connected MCP servers with their exposed tools for introspection and debugging.aiAgent() and aiModel() BIFs gain an array mcpServers = [] parameter so servers can be provided at construction time.AiAgent now tracks connected MCP servers in a mcpServers property ([{ url, toolNames }]). This list is automatically injected into the system prompt so the LLM can correctly answer questions like "what MCP servers are you connected to?" and "which tools came from which server?"listTools() method on AiAgent returns [{ name, description }] for all registered tools โ useful for programmatic introspection.AiAgent|AiModel.getConfig() now includes tools (full name/description list) and mcpServers (server URL + tool-name list) alongside the existing toolCount.AIToolRegistry (accessible via aiToolRegistry() BIF) provides a module-scoped registry for AI tools. Tools can be registered by name with optional module namespacing (e.g. now@bxai), discovered at runtime by bare name or full key, and resolved lazily before LLM requests via aiToolRegistry().resolveTools(). This means tools can be referenced by string name in params.tools arrays and resolved automatically rather than requiring live object references.BaseTool abstract base class: All tool implementations now extend BaseTool, which provides the shared invocation lifecycle (firing beforeAIToolExecute and afterAIToolExecute interception events), result serialization (primitives pass through, complex values serialize to JSON), and the fluent describeArg() / describe[ArgName]() schema annotation syntax.ClosureTool class: Replaces the retired Tool.bx. A BaseTool subclass backed by any closure or lambda. Auto-introspects the callable's parameter metadata to generate an OpenAI-compatible function schema. Receives the originating AiChatRequest as _chatRequest for context-aware closures.CoreTools built-in tools: Ships two tools out of the box. now (registered automatically as now@bxai on module load) returns the current date/time in ISO 8601 โ ideal for giving the AI temporal awareness. httpGet (opt-in only, not auto-registered for security) fetches any URL via HTTP GET. Register it explicitly if your application requires web access.params.tools arrays in aiChat(), aiModel().run(), and aiAgent().run() now accept string registry keys alongside live ITool instances. AIToolRegistry::resolveTools() converts any string keys to their registered ITool before the request is sent.onAIToolRegistryRegister and onAIToolRegistryUnregister.AiChatRequest object during invocation, allowing for more complex and context-aware tool behavior. They receive a _chatRequest argument that includes all the properties of the original request, such as messages, params, options, and more. This enables tools to make informed decisions based on the full conversation context and request configuration.AiModel and AiAgent, with agent middleware prepended ahead of model middleware.preRequest(), postResponse(),for any custom logic before and after requests to change the shape of the request or response, log additional data, etc. These hooks are provider-specific and allow for custom behavior without needing to override the entire sendChatRequest() method.add(), getAll(), clear(), trim(), seed(), and related methods on every IAiMemory and IVectorMemory implementation now accept optional userId and conversationId arguments. This follows the Spring AI ChatMemory pattern โ a single memory instance can safely serve multiple tenants without creating a new instance per user. Construction-time values remain as fallbacks.models/providers/capabilities/ package introduces IAiChatService and IAiEmbeddingsService โ scoped interfaces that let providers declare exactly which operations they support at the type level rather than through runtime throws.getCapabilities() / hasCapability() on all providers: Every provider now exposes getCapabilities() (returns ["chat", "stream", "embeddings", ...]) and hasCapability( "chat" ) for clean, self-documenting runtime introspection. These are backed by isInstanceOf() checks and stay automatically in sync with the implements declarations on each provider โ no maintenance required.AiAgent parent-child hierarchy: AiAgent now tracks its position in a multi-agent tree through a parentAgent property and a full set of hierarchy helpers:
setParentAgent(parent) โ assign a parent with self-reference and cycle-detection guardsclearParentAgent() โ detach from a parenthasParentAgent() โ returns true if the agent has a parentisRootAgent() โ returns true for top-level agentsgetRootAgent() โ walks up the tree and returns the root agentgetAgentDepth() โ returns the nesting depth (0 = root, 1 = direct child, โฆ)getAgentPath() โ returns a slash-delimited path string, e.g. /coordinator/researchergetAncestors() โ returns an ordered array [immediateParent, โฆ, root]addSubAgent() now automatically calls setParentAgent(this) on the sub-agentsetSubAgents() now calls clearParentAgent() on replaced sub-agents before replacing themgetConfig() now includes parentAgent (name string), agentDepth, and agentPathrunnables folder. This includes AiModel, AiAgent, and AiMessage. This better reflects their purpose as executable entities that can be run with different inputs, and allows for a cleaner separation between the core service logic and the runnable wrappers.BaseService to be truly a base and move all OpenAI specific logic to OpenAIService, which now serves as the default provider implementation. This allows for cleaner implementations of other providers that don't need to override every method.AiAgent is now fully stateless: userId, and conversationId are resolved per-call from the options argument passed to run() and stream(), eliminating shared-state concurrency bugs in multi-user deployments. Seeding a memory with userId and conversationId is still supported, but these values will be overridden by any values passed in at call time.resume() and resumeStream() now require threadId as an explicit required string argument instead of defaulting to the former instance property.IAiService contract trimmed: The base interface now declares only identity/configuration/capability-discovery methods (getName(), configure(), getCapabilities(), hasCapability()). The operation methods (invoke(), invokeStream(), embeddings()) have moved to their respective capability interfaces where they belong.VoyageService now extends BaseService directly and implements only IAiEmbeddingsService โ it no longer extends OpenAIService with stubbed-out chat methods that threw at runtime. The type system now enforces the embeddings-only constraint at compile time.aiChat(), aiChatStream(), and aiEmbed() BIF guards: Each BIF now checks the provider implements the required capability interface before attempting the call and throws a clear UnsupportedCapability exception instead of a cryptic provider error. Zero breaking changes to public BIF signatures.BaseService.sendRequest() to sendChatRequest().onAITokenCount.base_resp.status_code != 0) now surface correctly.OllamaService stale postEmbeddingResponse() hook: The old hook was never wired to the current BaseService lifecycle and silently did nothing. Replaced with the proper postResponse( aiRequest, dataPacket, result, operation ) override that guards on operation != "embeddings", identical to how every other dual-capability provider handles this.minimax provider name and set your API key via the MINIMAX_API_KEY environment variable.getConfig() to not show sensitive info._input System Variable: Auto-inject previous stage output into message templates via ${_input}. For struct outputs, individual fields are flattened as ${_input_fieldName} for template access. Enables clean, composable multi-stage AI pipelines without manual transformation steps.aiTransform() needd to process instances of AiTransformRunnable and BaseTransformer classes, allowing for more flexible and reusable transformation logic.config on all BaseTransformer classes was missing.aiTransform() BIF was called with a non-string or closure, the throw() was invalid.request in the aiChatStream() BIF, which should have been chatRequest.aiChat() and aiChatStream() BIFs was incorrect, causing default options to override user-provided options. Now it merges in the correct order: user options โ module settings โ default options, allowing for proper overrides.aiService() BIF was not correctly applying convention-based API key detection when options.apiKey was already set but empty. Now it checks if options.apiKey is empty before applying the convention key, allowing for proper fallback to environment variables or module settings.What's New: https://ai.ortusbooks.com/readme/release-history/2.1.0
onMissingAiProvider to handle cases where a requested provider is not found.aiModel() BIF now accepts an additional options struct to seed services.providers so you can predefine multiple providers in the module config, with default params and options."providers" : {
"openai" : {
"params" : {
"model" : "gpt-4"
},
"options" : {
"apiKey" : "my-openai-api-key"
}
},
"ollama" : {
"params" : {
"model" : "qwen3:0.6b"
},
"options" : {
"baseUrl" : "http://my-ollama-server:11434/"
}
}
}
options.baseUrl parameter.AiBaseRequest.mergeServiceParams() and AiBaseRequest.mergeServiceHeaders() methods now accept an override boolean argument to control whether existing values should be overwritten when merging.nomic-embed-text model for embeddings support.nomic-embed-text model.tenantId option for attributing AI usage to specific tenantsusageMetadata option for custom tracking data (cost center, project, userId, etc.)onAITokenCount events with tenant context for interceptor-based billingproviderOptions struct for provider-specific settings
providerOptions option for passing provider-specific configuration (e.g., inferenceProfileArn for Bedrock)getProviderOption(key, defaultValue) method on requests for retrieving provider optionsembeddingOptions configuration in BaseVectorMemory for passing options to embedding providerembeddingOptions.baseURL for custom OpenAI-compatible embedding service URLsInvokeModelWithResponseStream API endpointIAiService interface, ensuring consistent behavior across providers.IAiService.configure() method now accepts a generic options argument instead of apiKey, to better reflect its purpose and support more configuration options.AiRequest class renamed to AiChatRequest for clarity, and multi-modality support.onAIChatRequest, onAIChatRequestCreate, and onAIChatResponse.aiChat, aiChatStream BIF was not passing headers to the AiChatRequest.aiChat, aiChatStream, aiChatAsync BIF was not using aiChatRequest() to build the request, but was building it manually.aiChat(), aiChatStream() BIF.chr() --> char() in SSE formatting in MCPRequestProcessor and HTTPTransport.AiModel.getModel() was not returning the model name correctly when using predefined providers from config.url parameter conflict in OpenSearchVectorMemory by using requestUrl for HTTP requestsWhat's New: https://ai.ortusbooks.com/readme/release-history/2.0.0
One of our biggest library updates yet! This release introduces a powerful new document loading system, comprehensive security features for MCP servers, and full support for several major AI providers including Mistral, HuggingFace, Groq, OpenRouter, and Ollama. Additionally, we have implemented complete embeddings functionality and made numerous enhancements and fixes across the board.
aiDocuments() BIF for loading documents with automatic type detectionaiDocumentLoader() BIF for creating loader instances with advanced configurationaiDocumentLoaders() BIF for retrieving all registered loaders with metadataaiMemoryIngest() BIF for ingesting documents into memory with comprehensive reporting:
aiChunk() integrationaiTokens() integrationDocument class for standardized document representation with content and metadataIDocumentLoader interface and BaseDocumentLoader abstract class for custom loadersTextLoader: Plain text files (.txt, .text)MarkdownLoader: Markdown files with header splitting, code block removalHTMLLoader: HTML files and URLs with script/style removal, tag extractionCSVLoader: CSV files with row-as-document mode, column filteringJSONLoader: JSON files with field extraction, array-as-documents modeDirectoryLoader: Batch loading from directories with recursive scanningloadTo() method and aiMemoryIngest() BIFdocs/main-components/document-loaders.mdwithCors(origins) - Configure allowed origins (string or array)addCorsOrigin(origin) - Add origin dynamicallygetCorsAllowedOrigins() - Get configured origins arrayisCorsAllowed(origin) - Check if origin is allowed with wildcard matching*.example.com)*)Access-Control-Allow-Origin header in responseswithBodyLimit(maxBytes) - Set maximum request body size in bytesgetMaxRequestBodySize() - Get current limit (0 = unlimited)withApiKeyProvider(provider) - Set custom API key validation callbackhasApiKeyProvider() - Check if provider is configuredverifyApiKey(apiKey, requestData) - Manual key validationX-API-Key header and Authorization: Bearer tokenX-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=blockReferrer-Policy: strict-origin-when-cross-originContent-Security-Policy: default-src 'none'; frame-ancestors 'none'Strict-Transport-Security: max-age=31536000; includeSubDomainsPermissions-Policy: geolocation=(), microphone=(), camera=()docs/advanced/mcp-server.md with examplesMistralService provider class with OpenAI-compatible APImistral-embed modelmistral-small-latestMISTRAL_API_KEY environment variableHuggingFaceService provider class extending BaseServicerouter.huggingface.co/v1Qwen/Qwen2.5-72B-InstructHUGGINGFACE_API_KEYapi.groq.comllama-3.3-70b-versatileGROQ_API_KEYaiEmbedding() BIF for generating text embeddingsAiEmbeddingRequest class to model embedding requestsembeddings() method in IAiService interfacetext-embedding-3-small and text-embedding-3-large modelstext-embedding-004 modelonAIEmbeddingRequest, onAIEmbeddingResponse, beforeAIEmbedding, afterAIEmbeddingexamples/embeddings-example.bx demonstrating practical use casesformat(bindings) - Formats messages with provided bindings.render() - Renders messages using stored bindings.bind( bindings ) - Binds variables to be used in message formatting.getBindings(), setBindings( bindings ) - Getters and setters for bindings.AIService() BIF: <PROVIDER>_API_KEY from system settingsTool.getArgumentsSchema() method to retrieve the arguments schema for use by any provider.logRequestToConsole, logResponseToConsoleChatMessage helper method: getNonSystemMessages() to retrieve all messages except the system message.ChatRequest now has the original ChatMessage as a property, so you can access the original message in the request.claude-sonnet-4-0 as its default.logRequest, logResponse, timeout, returnFormat, so you can control the behavior of the services globally.onAIResponse event.1.0.0 in the box.json file by accident.settings in the module config.
$
box install bx-ai