extensions/
Add commands and schedules to the chat and the served agent, as one file or a folder, with settings.
On this page
extensions/
An extension adds to BxAgents without changing it: chat commands (/deploy, /roll 2d6), listeners for the events of the chat and of bx-ai, and schedules that run when the agent is served. It is a class that looks and behaves like a ColdBox ModuleConfig: it has a configure(), an onLoad() and an onUnload(), its settings come from Agent.yaml, and things are handed to it with $inject.
One file is a whole extension:
extensions/
hello.bx /hello
roll.bx /roll, /coin
standup/ a folder is an extension too
Extension.bx
commands/Standup.bx
schedulers/Reminder.bx
A working project with all of these is examples/extensions-agent.
The smallest extension
extensions/hello.bx. The file name is the extension's name.
class {
function configure() {
variables.settings = { greeting : "Hello" }
}
@Command( "hello" )
@Hint( "say hello: /hello <name>" )
function greet( agentContext, commandArguments ) {
return agentContext.reply().ok( "#settings.greeting#, #commandArguments#!" )
}
}
Run bxAgents chat and type /hello Ada. /extensions lists what is loaded.
A command method is called with the agentContext (the conversation and nothing else; the same object the built-in commands get) and the text after the command name. It returns a Reply (agentContext.reply().ok( "..." ), .warn(), .keyValues(), .grid(), .choose()) or just a string. If it throws, the person sees a warning that names the extension and the chat carries on.
Annotations
Annotations are normal BoxLang annotations on a method. Each one can be written four ways, and every BxAgents annotation takes all four:
| Written | Means | Example |
|---|---|---|
| bare | defaults; the name comes from the method | @Command on function roll() is /roll |
| simple | the main value | @Command( "deploy" ) |
| array | several | @Alias( [ "dice", "d" ] ) |
| struct | every option | @Command( { name : "deploy", hint : "plan it", admin : true, aliases : [ "ship" ], transports : [ "tui", "web" ] } ) |
Shorthand annotations say one option each: @Hint( "..." ), @Admin, @Alias( ... ), @Transports( [ "tui", "web" ] ). A struct option wins over a shorthand, and the clash is shown in /extensions. Annotation names ignore case.
transports are where the command runs: tui (the terminal chat), web (the web page and the HTTP command route) and gateway (chat platforms). The default is all three. A chat platform only answers the commands its gateway allows (gatewaySession.commands.allow), and a command with @Admin only runs for the people listed as admins there.
Settings
The extension says its defaults in configure(), as a module does:
function configure() {
variables.settings = { target : "staging", environments : [ "staging", "production" ] }
}
The people who use it change them under the extension's name in Agent.yaml:
extensions:
deploy:
target: production
A person can set their own values in ~/.bxagents/bxAgents.yml (same extensions: block); the project's values win over theirs. Nested structs merge key by key, arrays and simple values replace. In the extension, settings is the merged struct.
Lifecycle
| Method | When |
|---|---|
configure() | once, first. Sets variables.settings. No work here |
onLoad() | after the settings are merged and everything is injected. Open connections, start timers |
onUnload() | when the chat or the server ends. Release them |
this.title, this.author, this.webURL, this.description, this.version and this.enabled are read as in a module config, and this.dependencies = [ "other-extension" ] loads that extension first. An extension that throws while it loads is shown as failed in /extensions with the reason; the others still load.
Injection
Every class of an extension has $inject( name, object )`. It puts the object in both the `this` and the `variables` scopes. For private only: `$inject( name, object, "variables" ). $read( name ) reads a variables entry from outside.
BxAgents injects two things before onLoad(): settings and log (log.info( "..." ), .warn(), .error(); what an extension logs is also in /extensions). To share an object between the files of an extension, create it in onLoad() and $inject it where it is needed. The same call is how a test hands an extension a fake.
A folder extension
A folder has the same shape as an agent project:
extensions/standup/
Extension.bx optional: the module config (properties, configure(), onLoad(), onUnload())
commands/ one class per command, like the built-in ones
schedulers/ BoxLang scheduler classes, started when the agent is served
A class in commands/ extends models.chat.commands.BaseCommand@bxagents and says its name(), hint(), transports() and run( context, args ). settings and log are injected into it.
Schedulers
A class in schedulers/ is a BoxLang scheduler: a class with a configure() that defines tasks through the scheduler property BoxLang gives it, like any other BoxLang scheduler. When the agent is served (bxAgents serve, a .bxa, a deployed server) it starts in the runtime's scheduler service. Like every class of an extension it is given settings (the defaults with the values from Agent.yaml) and log before configure() runs:
class {
function configure() {
scheduler.task( "standup-reminder" )
.call( () => log.info( "Standup time for #settings.channel#" ) )
.everyDayAt( settings.at )
.withNoOverlaps()
}
}
The BoxLang scheduler lifecycle methods (onStartup(), onShutdown(), onAnyTaskError() and the rest) work as usual. Each scheduler is registered as extension-<extension>-<scheduler>, and a scheduler that fails to start is reported on its extension in /extensions while the others still run. The terminal chat does not start schedulers: they belong to the served agent.
Listening to events
An extension is also an interceptor. A method named after an event listens to it, exactly like a BoxLang or ColdBox interceptor:
class {
function onChatInput( data ) {
if ( data.text contains "password" ) {
data.cancel( "That looks like a password, so it was not sent." )
}
}
function onAIError( data ) {
log.warn( "the model failed" )
}
}
Two things can be listened to:
- bx-ai events (
beforeAIAgentRun,afterAIModelInvoke,onAIError,beforeAIToolExecuteand the rest). They fire wherever the agent runs. - Chat events, announced by BxAgents:
| Event | When | data has | A listener can |
|---|---|---|---|
onChatSessionStart / onChatSessionEnd | the terminal chat opens or closes | agent, transport | |
onChatInput | before a message is sent | text, transport | cancel( reason ), or replace( newText ) |
onChatTurnStart / onChatTurnEnd | around one turn of the agent | text, transport (reply, status at the end) | |
onChatConversationChange | /new, /resume, /clear, /fork or /compact changed the conversation | line, transport | |
onChatCommandBefore | before any command, on every transport | line, name, arguments, transport | cancel( reason ) |
onChatCommandAfter | after any command, on every transport | line, name, reply, transport | replace( newReply ) |
The session, input, turn and conversation events are the terminal chat's; the two command events fire in the terminal, the web page and on chat platforms alike. A listener that throws is caught: it can never stop a turn, the listeners after it still run, and the error is shown in /extensions.
@On binds a method with another name, or many events at once, and takes the four forms an annotation takes:
@On( [ "beforeAIToolExecute", "afterAIToolExecute" ] )
function toolCalled( data ) { log.info( "a tool ran" ) }
@On naming an event the runtime does not know is reported in /extensions, not ignored.
interceptors and customInterceptionPoints
As in a module config, configure() can name classes from the extension's folder to register, each with its own properties, and points of its own to announce:
function configure() {
variables.interceptors = [ { class : "interceptors.Audit", properties : { level : "info" } } ]
variables.customInterceptionPoints = [ "onAuditRecorded" ]
}
interceptors/Audit.bx listens with methods named after events and has settings, log and properties injected. Any extension can then listen to onAuditRecorded, and the one that owns it announces it with BoxAnnounce( "onAuditRecorded", { ... } ). extensions.disable takes extension:method to turn one listener off.
ColdBox events (preProcess, postProcess and the rest) are not part of the terminal chat; they fire only in a served agent.
Where extensions come from
| Layer | Folder | Runs in |
|---|---|---|
| modules | extensions/ of an installed BoxLang module | everywhere the module is installed |
| user | ~/.bxagents/extensions/ | the terminal chat on that machine only |
| project | <project>/extensions/ | the terminal chat, the web page, chat platforms, the server |
A later layer wins. An extension with the same name replaces the earlier one, and a command replaces an earlier command of the same name or alias, a built-in included. Both are shown in /extensions; nothing is replaced silently. The build copies the project's extensions/ into the app; a served agent never reads a developer's home folder.
Turning things off
extensions:
disable: [ roll, "deploy:deploy", "acme-*" ]
A plain name turns off the extension of that name (and any command called that); extension:item turns off one command; * matches any run of characters. Both Agent.yaml and ~/.bxagents/bxAgents.yml accept it.
Looking at them
/extensions in the chat, or from the shell:
bxAgents extensions # the list, where each came from and what it added
bxAgents extensions --json
Not yet
Extensions cannot yet add key bindings, status items, composer prefixes, @ sources, web UI slots, gateways, CLI verbs, tools and skills. Those come next; the plan is EXTENSIONS-PLAN.md in the repository.