gateways/
Ein Ordner, zwei unabhängige Aufgaben: den Agenten exponieren, und Chat-Plattformen mit ihm verbinden.
On this page
- 1. HTTP-/MCP-/Web-UI-Exposure ( exposes: "agent" | "mcp" | "webui" )
- 2. Channel-Adapter-Gateways ( type: "mock" | "cli" | "http" )
- 3. Push-style gateways ( type: "telegram" / "slack" / "discord" / "email" / "whatsapp-cloud" / "teams" / "twilio" / "github" / "signal" , and friends)
- Slacks persistente Verbindung
- Discords persistente Verbindung - obligatorische, clientseitig getriebene Heartbeats
- E-Mail - serverseitige Abhängigkeiten, und verschlechtertes Threading/HITL
- WhatsApp Business Cloud API - Webhook-getrieben, nicht verbindungsgetrieben
- Microsoft Teams - Bot-Framework-Activity-Protokoll
- Twilio SMS - ein genuin anderes Signaturschema, und ein zweigleisiges Response-Modell
- GitHub - über @mention geschützte Issue-/PR-Kommentar-Threads
- Signal - eine vierte Transportform, gegen einen externen signal-cli -Daemon
- GatewaySession - wiring the agent to every push-style gateway
- Wie ein Push-Style-Gateway verbunden bleibt: der geteilte ColdBox-Scheduler
- Logging
gateways/
gateways/*.bx/.json-Dateien unter diesem einen Ordner decken zwei unterschiedliche, unabhängige Dinge ab - welcher Art ein Eintrag ist, hängt allein davon ab, ob seine configure()-Struktur einen exposes-Schlüssel hat.
Diese nicht miteinander verwechseln - ein per HTTP exponierter Agent (exposes: "agent") ist eine REST-API für den Agenten; ein Channel-Adapter-Gateway (type: "http") ist ein Webhook-Endpunkt für eine Chat-Plattform oder einen Human-in-the-Loop-Genehmigungsablauf. Sie erzeugen völlig unterschiedliche Routen.
flowchart TD
F["a file under gateways/"] --> Q{"does configure() return<br/>an 'exposes' key?"}
Q -->|"yes"| E["EXPOSURE<br/>a route into your agent"]
Q -->|"no - it has a 'type' key instead"| C["CHANNEL ADAPTER<br/>a connection to a chat platform"]
E --> E1["exposes: agent<br/>route().toAi()"]
E --> E2["exposes: mcp<br/>route().toMCP()"]
E --> E3["exposes: webui<br/>generated index.html + /api"]
C --> C1["mock / cli / http<br/>pull-driven: something calls US"]
C --> C2["telegram, slack, discord, email, whatsapp-cloud,<br/>teams, twilio, github, signal<br/>push-style: holds its own connection"]
C2 --> S["one GatewaySession<br/>bound to the root agent"]
style E fill:#d4edda,stroke:#155724
style C fill:#cce5ff,stroke:#004085
1. HTTP-/MCP-/Web-UI-Exposure (exposes: "agent" | "mcp" | "webui")
Exponiert den Agenten, oder einen lokalen MCP-Server, über HTTP mit ColdBox 8.1s nativer AI-Routing-DSL - oder eine fertige Browser-Chat-UI, separat dokumentiert in Die Web-Chat-UI.
Den Agenten exponieren:
// gateways/expose.bx
class {
function configure() {
return {
exposes : "agent",
path : "/api/chat"
};
}
}
Erzeugt, in config/Router.bx:
route( "/api/chat" ).toAi( "GeneratedAgent" )
was automatisch vier Unterrouten registriert: POST /api/chat/invoke, POST /api/chat/stream (SSE), POST /api/chat/batch, GET /api/chat/info. Der bloße Pfad /api/chat selbst ist nicht routbar.
Einen lokalen MCP-Server exponieren (siehe mcp/):
class {
function configure() {
return {
exposes : "mcp",
path : "/mcp/tools",
target : "local-server" // must match an mcp/*.bx entry's declared name
};
}
}
Erzeugt route( "/mcp/tools" ).toMCP( "local-server" ).
Die v1-Web-Chat-UI exponieren:
// gateways/chat.bx
class {
function configure() {
return {
exposes : "webui",
path : "/chat",
apiKeyEnvVar: "CHAT_UI_API_KEY" // optional - see below
};
}
}
Erzeugt eine echte statische <path>/index.html-Datei (direkt bedient - keine Route dafür nötig) plus eine eigene, dedizierte API unter einem festen <path>/api-Präfix, sodass sie nie mit den Dateien der Hülle selbst kollidiert. Diese API ist ein generiertes handlers/ChatUi.bx statt toAi(), und der Eintrag bringt außerdem einen generierten SQLite-Store mit.
Die Web-UI ist eher ein Subsystem als ein einzelner Exposure-Schalter - die Routenliste, der Store, Konversationen und Präferenzen, Branding und Theming, und warum sie toAi() nicht nutzt, stehen alle auf ihrer eigenen Seite: Die Web-Chat-UI.
Validierung: exposes muss agent, mcp oder webui sein; path ist erforderlich und muss über jeden Exposure-Eintrag hinweg eindeutig sein; das target einer mcp-Exposure ist erforderlich und muss zum deklarierten Namen eines echten mcp/*-Eintrags passen; apiKeyEnvVar bei webui ist vollständig optional, ohne Pflichtfeld-Prüfung (siehe unten).
2. Channel-Adapter-Gateways (type: "mock" | "cli" | "http")
Registriert namentlich ein bx-ai-IGateway (einen Channel-Adapter für externe Auslieferung / Human-in-the-Loop-Genehmigung) - zu unterscheiden vom Exponieren der eigenen REST-API des Agenten.
// gateways/slack.bx
class {
function configure() {
return {
type : "http",
secretEnvVar : "SLACK_WEBHOOK_SECRET"
};
}
}
secretEnvVar benennt eine Umgebungsvariable, die das Signing-Secret enthält - nie den Secret-Wert selbst. Erzeugt, in Application.bxs onApplicationStart():
aiGatewayRegistry().register( aiGateway( "http", { secret : getSystemSetting( "SLACK_WEBHOOK_SECRET", "" ) } ) )
Das Secret wird live beim Serverstart aufgelöst, passend zur "Secrets bleiben extern"-Regel dieses Projekts überall sonst (siehe Deployment & Secrets) - es wird nie als Literal in generierten Quellcode eingebettet, ist also auch nie in einer paketierten .bxa vorhanden. Ist die Umgebungsvariable ungesetzt, behandelt bx-ais eigenes HttpGateway ein leeres Secret als "keine Signierung konfiguriert" und lehnt Requests entsprechend ab, statt beim Start abzustürzen.
Validierung: type muss mock, cli oder http sein; ein Eintrag mit type: "http" erfordert ein secretEnvVar; der eigene Datei-/Basisname des Eintrags muss über jeden Channel-Adapter-Eintrag hinweg eindeutig sein. mock ist nur für Tests; cli ist bx-ais eigener eingebauter Human-in-the-Loop-Genehmigungs-Kanal (ein blockierender A/R/Q-Prompt über stdin/stdout) - er ist es, was HumanInTheLoopMiddleware standardmäßig anhängt, wenn kein Gateway angegeben ist, und hat nichts mit BX Agents' eigenem chat-Verb zu tun (das die Gateway-Registry überhaupt nie berührt).
Einträge vom Typ http erhalten zusätzlich echte HTTP-Verdrahtung: eine generierte handlers/Gateway.bx-Action, die direkt in bx-ais eigenes GatewayRequestProcessor::processHttp() durchreicht, sowie drei Routen in config/Router.bx:
post( "/gateways/:gatewayName/events" ).toHandler( "Gateway.process" )
get( "/interactions/:requestID" ).toHandler( "Gateway.process" )
post( "/interactions/:requestID/decisions" ).toHandler( "Gateway.process" )
ColdBox hat keinen eingebauten toAiGateway()-DSL-Terminator für diese Oberfläche (nur toAi() und toMCP() existieren nativ) - diese Verdrahtung ist BX Agents' eigener generierter Code, in derselben Form, die ein zukünftiger Core-Terminator erzeugen würde. Siehe den Vorschlag toAiGateway() für ColdBox Core.
3. Push-style gateways (type: "telegram" / "slack" / "discord" / "email" / "whatsapp-cloud" / "teams" / "twilio" / "github" / "signal", and friends)
Eine andere Art von Channel-Adapter als mock/cli/http oben: Statt von einem eingehenden HTTP-Request getrieben zu werden, hält ein Push-Style-Gateway seine eigene Verbindung zur Plattform und pusht eingehende Nachrichten an den Agenten, sobald sie eintreffen - das eher "echter Chat-Bot"-artige Erlebnis. Vier Transportformen existieren heute:
- Long-Poll (Telegram, E-Mail): ein geplanter Task fragt die Plattform periodisch "irgendwas Neues?" (Telegrams
getUpdates, E-Mails IMAP-Poll). - Persistenter Websocket (Slack über Socket Mode, Discord über seine Gateway-API): das Gateway hält eine lebende, langlaufende Verbindung, über die die Plattform Events in Echtzeit herunterpusht.
- Webhook, pull-getrieben (WhatsApp Business Cloud API, Microsoft Teams, Twilio SMS, GitHub): die Plattform ruft uns über einen öffentlichen HTTP-Endpunkt an, statt dass dieses Gateway eine eigene ausgehende Verbindung hält - kein Scheduler-Task oder Socket zu verwalten. Siehe deren eigene Unterabschnitte unten.
- Server-Sent Events (SSE) (Signal, gegen einen lokal laufenden
signal-cli-Daemon): eine langlebige, unidirektionale Streaming-HTTP-Verbindung, die das Gateway offen hält und dabei Events liest, während sie über denselben Response-Body gepusht werden. Siehe den eigenen Unterabschnitt unten.
// gateways/telegramChannel.bx
class {
function configure() {
return {
type : "telegram",
botTokenEnvVar: "TELEGRAM_BOT_TOKEN"
};
}
}
// gateways/slackChannel.bx
class {
function configure() {
return {
type : "slack",
botTokenEnvVar: "SLACK_BOT_TOKEN", // xoxb-... - chat.postMessage/chat.update
appTokenEnvVar: "SLACK_APP_TOKEN" // xapp-... - apps.connections.open (Socket Mode)
};
}
}
// gateways/discordChannel.bx
class {
function configure() {
return {
type : "discord",
botTokenEnvVar: "DISCORD_BOT_TOKEN" // Authorization: Bot <token> on every REST call and inside Identify
// intents: 37377 // optional override - defaults to GUILDS+GUILD_MESSAGES+DIRECT_MESSAGES+MESSAGE_CONTENT
};
}
}
// gateways/emailChannel.bx
class {
function configure() {
return {
type : "email",
imapHostEnvVar : "IMAP_HOST",
imapUsernameEnvVar: "IMAP_USERNAME",
imapPasswordEnvVar: "IMAP_PASSWORD",
fromAddressEnvVar : "EMAIL_FROM_ADDRESS"
// imapPort: 993 // optional override - defaults to 993 (IMAPS)
// pollIntervalSeconds: 60 // optional override - defaults to 60
};
}
}
// gateways/whatsappCloud.bx
class {
function configure() {
return {
type : "whatsapp-cloud",
accessTokenEnvVar : "WHATSAPP_ACCESS_TOKEN", // Graph API access token
phoneNumberIdEnvVar: "WHATSAPP_PHONE_NUMBER_ID", // the WhatsApp Business phone number ID sends go through
appSecretEnvVar : "WHATSAPP_APP_SECRET", // HMAC key verifying X-Hub-Signature-256 on inbound webhooks
verifyTokenEnvVar : "WHATSAPP_VERIFY_TOKEN" // shared secret Meta's GET verify handshake must echo back
// apiVersion: "v21.0" // optional override - defaults to "v21.0"
};
}
}
// gateways/teamsChannel.bx
class {
function configure() {
return {
type : "teams",
appIdEnvVar : "TEAMS_APP_ID", // the bot's own Microsoft App ID (also the inbound JWT's required aud claim)
appPasswordEnvVar : "TEAMS_APP_PASSWORD" // OAuth2 client-credentials secret
// tenantId: "..." // optional override for single-tenant apps - defaults to "botframework.com" (multi-tenant)
};
}
}
// gateways/twilioChannel.bx
class {
function configure() {
return {
type : "twilio",
accountSidEnvVar: "TWILIO_ACCOUNT_SID",
authTokenEnvVar : "TWILIO_AUTH_TOKEN", // also the X-Twilio-Signature HMAC key
fromEnvVar : "TWILIO_FROM_NUMBER" // the Twilio phone number outbound sends go through, E.164
// messagingServiceSid: "MG..." // optional - if set, used instead of `from` on outbound sends
// publicUrl: "https://your-real-public-host/webhooks/twilio" // optional override for reverse-proxy/tunnel deployments - see the Twilio subsection below
};
}
}
// gateways/githubChannel.bx
class {
function configure() {
return {
type : "github",
tokenEnvVar : "GITHUB_TOKEN", // a personal access token (repo/issues+PR read+write scope)
webhookSecretEnvVar: "GITHUB_WEBHOOK_SECRET", // HMAC key verifying X-Hub-Signature-256 on inbound webhooks
botNameEnvVar : "GITHUB_BOT_NAME" // the bot's own GitHub login - matched as "@botName" in comments
// apiBaseUrl: "https://api.github.com" // optional override - defaults to "https://api.github.com"
};
}
}
// gateways/signalChannel.bx
class {
function configure() {
return {
type : "signal",
accountEnvVar: "MY_SIGNAL_ACCOUNT" // the signal-cli-registered phone number this gateway sends/receives as, E.164
// httpUrl: "http://127.0.0.1:8080" // optional override - defaults to "http://127.0.0.1:8080", where signal-cli's own daemon HTTP API is expected to be listening
};
}
}
Dieselbe "Secrets bleiben extern"-Regel wie bei https secretEnvVar: Jeder *EnvVar-Schlüssel benennt eine Umgebungsvariable, live über getSystemSetting() beim Start aufgelöst, nie als Literal eingebettet - emails imapHost/fromAddress sind keine kryptografischen Secrets, aber dieselbe umgebungsvariablen-getriebene Konvention wird trotzdem für jeden einzelnen ihrer Konfigurationswerte verwendet, da sie alle pro Deployment variieren. Anders als bei den Core-Typen lebt die Klasse eines Push-Style-Gateways innerhalb von BX Agents selbst (models/gateways/*.bx, nicht bx-ai), sodass ihre Registrierung als bloßer Klassenpfad statt als Kurzname gerendert wird:
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TelegramGateway", { "botToken" : getSystemSetting( "TELEGRAM_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.SlackGateway", { "appToken" : getSystemSetting( "SLACK_APP_TOKEN", "" ), "botToken" : getSystemSetting( "SLACK_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.DiscordGateway", { "botToken" : getSystemSetting( "DISCORD_BOT_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.EmailGateway", { "imapHost" : getSystemSetting( "IMAP_HOST", "" ), "imapUsername" : getSystemSetting( "IMAP_USERNAME", "" ), "imapPassword" : getSystemSetting( "IMAP_PASSWORD", "" ), "fromAddress" : getSystemSetting( "EMAIL_FROM_ADDRESS", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.whatsapp.WhatsAppCloudGateway", { "accessToken" : getSystemSetting( "WHATSAPP_ACCESS_TOKEN", "" ), "phoneNumberId" : getSystemSetting( "WHATSAPP_PHONE_NUMBER_ID", "" ), "appSecret" : getSystemSetting( "WHATSAPP_APP_SECRET", "" ), "verifyToken" : getSystemSetting( "WHATSAPP_VERIFY_TOKEN", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TeamsGateway", { "appId" : getSystemSetting( "TEAMS_APP_ID", "" ), "appPassword" : getSystemSetting( "TEAMS_APP_PASSWORD", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.TwilioGateway", { "accountSid" : getSystemSetting( "TWILIO_ACCOUNT_SID", "" ), "authToken" : getSystemSetting( "TWILIO_AUTH_TOKEN", "" ), "from" : getSystemSetting( "TWILIO_FROM_NUMBER", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.GitHubGateway", { "token" : getSystemSetting( "GITHUB_TOKEN", "" ), "webhookSecret" : getSystemSetting( "GITHUB_WEBHOOK_SECRET", "" ), "botName" : getSystemSetting( "GITHUB_BOT_NAME", "" ) } ) )
aiGatewayRegistry().register( aiGateway( "bxModules.bxagents.models.gateways.SignalGateway", { "account" : getSystemSetting( "MY_SIGNAL_ACCOUNT", "" ) } ) )
Validierung: type: "telegram" erfordert botTokenEnvVar; type: "slack" erfordert sowohl botTokenEnvVar als auch appTokenEnvVar; type: "discord" erfordert botTokenEnvVar; type: "email" erfordert imapHostEnvVar, imapUsernameEnvVar, imapPasswordEnvVar und fromAddressEnvVar; type: "whatsapp-cloud" erfordert accessTokenEnvVar, phoneNumberIdEnvVar, appSecretEnvVar und verifyTokenEnvVar; type: "teams" erfordert appIdEnvVar und appPasswordEnvVar; type: "twilio" erfordert accountSidEnvVar, authTokenEnvVar und fromEnvVar; type: "github" erfordert tokenEnvVar, webhookSecretEnvVar und botNameEnvVar; type: "signal" erfordert accountEnvVar - alle auf dieselbe Weise geprüft wie https secretEnvVar.
Slack v1 ist nur Socket Mode - für Slack wird kein öffentlicher Webhook-Endpunkt benötigt oder generiert (anders als http, das echte Routen erhält - siehe §2 oben). Die von Slack ebenfalls unterstützte Events-API-/HTTP-Webhook-Alternative ist hier nicht gebaut. Discord v1 ist ebenso die echte Gateway-API (ein persistenter Websocket), statt Discords alternativem HTTP-Interactions-Endpoint-URL-Webhook-Modus - eine Ed25519-Signaturprüfung wird hier folglich nicht benötigt, da Interaktionen über dieselbe authentifizierte Verbindung eintreffen statt über einen öffentlichen HTTP-Endpunkt (gegen Discords eigene Dokumentation bestätigt).
Slacks persistente Verbindung
SlackGateway hält seinen Websocket über den asynchronen WebSocket-Client von java.net.http.HttpClient, überbrückt von einer BoxLang-Listener-Klasse, die direkt implements="java:java.net.http.WebSocket$Listener" (models/gateways/support/SlackSocketListener.bx) - BoxLang kompiliert das als echten JVM-Implementierer der Schnittstelle, empirisch bestätigt, indem eine Instanz direkt an HttpClient.newWebSocketBuilder().buildAsync( uri, listener ) übergeben wurde, ohne Casting-Fehler (nur der erwartete java.net.ConnectException, sobald die echte Netzwerkgrenze erreicht war). Nur die Methoden, die die Klasse tatsächlich deklariert, überschreiben die default-Methoden der JDK-Schnittstelle; alles nicht Implementierte fällt automatisch auf das eigene Standardverhalten des JDK zurück. Das ist das Referenzmuster, dem jedes andere Gateway mit persistenter Verbindung (Discord, unten) ebenfalls folgt.
Reconnects werden reaktiv von Slacks eigenen Protokollsignalen getrieben - ein disconnect-Frame (warning/refresh_requested) oder ein unerwarteter Socket-Close - wobei eine neue Verbindung geöffnet wird, bevor die alte geschlossen wird, gemäß Slacks eigener dokumentierter Empfehlung. Ein leichtgewichtiger Scheduler-Watchdog (slack-watchdog-<name>, alle 30s) ist nur ein Sicherheitsnetz für den Fall, dass keines dieser beiden Signale feuert.
Discords persistente Verbindung - obligatorische, clientseitig getriebene Heartbeats
DiscordGateway verbindet sich auf dieselbe Weise (models/gateways/support/DiscordSocketListener.bx, dasselbe Muster implements="java:java.net.http.WebSocket$Listener" wie Slack), aber Discords Gateway-Protokoll hat eine Anforderung, die Slacks Socket Mode nicht hat: Der eigene Hello-Frame des Servers (Opcode 10) teilt dem Client ein heartbeat_interval mit, und der Client muss selbst in diesem Takt Heartbeat-Frames (Opcode 1) senden, sonst behandelt Discord die Verbindung als "zombiert" und trennt sie. Da das Intervall erst bekannt ist, sobald Hello eintrifft (nicht vor dem Verbinden), wird der Heartbeat als eigener Scheduler-Task (discord-heartbeat-<name>) dynamisch aus dem Frame-Handler heraus registriert, bei jedem neuen Hello neu registriert - anders als bei jedem anderen Push-Style-Gateway mit seinem/seinen zur registerScheduledTasks()-Zeit fixierten Task(s), und anders als Discords eigenem Sicherheitsnetz-Watchdog (discord-watchdog-<name>, alle 30s, dieselbe Rolle wie bei Slack).
Jeder Heartbeat-Tick prüft, ob der vorherige Heartbeat je bestätigt wurde (Heartbeat ACK, Opcode 11) - falls nicht, ist die Verbindung zombiert und wird proaktiv neu verbunden, statt sie in ein Timeout laufen zu lassen. Reconnects folgen ansonsten Discords eigenem dokumentiertem Sitzungsmodell: Ein Reconnect-Frame (Opcode 7) oder die meisten Close-Codes lösen ein Resume aus (Opcode 6, das die letzte Sequenznummer wiedergibt) auf der neuen Verbindung, falls eine vorherige Sitzung existiert; ein Invalid Session-Frame (Opcode 9) mit d: false, oder ein von Discord als sitzungsinvalidierend dokumentierter Close-Code (4007, 4009), erzwingt stattdessen ein frisches Identify (Opcode 2). Eine kleine, feste Menge von Close-Codes (4004 falsches Token, 4010 ungültiger Shard, 4011 Sharding erforderlich, 4012 ungültige API-Version, 4013/4014 ungültige/nicht erlaubte Intents) sind laut Discords eigener Dokumentation nicht wiederherstellbar - das Gateway stoppt, statt eine Verbindung erneut zu versuchen, die ohnehin wieder fehlschlagen würde.
MESSAGE_CONTENT (nötig, um Nachrichtentext überhaupt zu lesen, sowohl in Guild-Kanälen als auch in DMs) ist ein privilegierter Discord-Gateway-Intent - er muss für den eigenen Bot im Discord Developer Portal explizit aktiviert werden, und sobald die eigene App verifiziert ist (100+ Guilds), von Discord genehmigt werden. Ohne ihn kommt jede eingehende Nachricht mit einem leeren content-Feld an.
E-Mail - serverseitige Abhängigkeiten, und verschlechtertes Threading/HITL
EmailGateway ist das einzige Push-Style-Gateway, das nicht direkt mit der API seiner Plattform spricht. Ausgehende Mails laufen durch ColdBoxs eigenes Modul cbmailservices (MailService@cbmailservices, dessen BXMail-Protokoll - das wiederum nur BoxLangs eigene bx:mail-Komponente aus dem bx-mail-Modul aufruft), statt eines handgerollten HTTP-/SMTP-Aufrufs. Beide sind echte, serverseitige Modul-Installationen - sie sind als eigene box.json-dependencies dieses Projekts deklariert (die Installation von bx-agents zieht sie also auch auf den Server), aber cbmailservices/bx-mail benötigen trotzdem beide eine explizite Installation auf welchem Server auch immer eine generierte App tatsächlich betreibt (gegen die eigene Dokumentation/den Quellcode beider Module bestätigt - keines wird vorinstalliert mit ColdBox oder BoxLang ausgeliefert) - vor bxAgents serve/dem Deployment eines Projekts mit einem email-Gateway ein echtes box install (oder Äquivalent) durchführen. EmailGateway löst MailService@cbmailservices manuell über application.cbController.getWireBox() auf (siehe den eigenen Docblock von ScheduledGatewayBase.resolveScheduler() dafür, warum - diese Klasse wird direkt von aiGateway() konstruiert, vollständig außerhalb von WireBox, inject="" wird auf ihr also nie honoriert), auf dieselbe Weise, wie auch der Scheduler selbst aufgelöst wird.
Da weder bx-mail noch cbmailservices Mail empfangen (nur senden), ist Inbound handgerolltes IMAP über die JDK-Standard-API jakarta.mail - bestätigt, transitiv im eigenen Klassenpfad dieses Projekts erreichbar (bx-mail hängt von commons-email2-jakarta ab, das wiederum von jakarta.mail-api + einer Angus-Mail-Implementierung abhängt), in dieser Session empirisch gegen die echten Jars verifiziert, nicht angenommen. Ein geplanter Task (email-poll-<name>) pollt IMAP nach ungelesener Mail, dieselbe Form wie Telegrams Long-Poll.
Threading und Human-in-the-Loop sind beide im Vergleich zu den Chat-Plattform-Gateways verschlechtert, und getDeclaredCapabilities() lässt bewusst "interactiveActions" weg, um das ehrlich anzuzeigen:
- Threading nutzt echte
Message-ID-/In-Reply-To-/References-Header für eine GEWÖHNLICHE Antwort (das Gateway kennt immer dieMessage-IDder eingehenden Nachricht, auf die es antwortet, das Setzen vonIn-Reply-Toauf der ausgehenden Antwort ist also zuverlässig) - eine v1-Vereinfachung threadet auf dem ersten Eintrag vonReferences(sonstIn-Reply-To, sonst die eigeneMessage-IDder Nachricht), keinen vollständigen Walk der Kette. - Human-in-the-Loop hat überhaupt keine native Button-/Komponentenoberfläche -
requestHumanInteraction()sendet eine reine Text-E-Mail, die die erlaubten Entscheidungs-Schlüsselwörter auflistet, und bittet den Menschen, mit einem davon als erster Zeile zu antworten. Diese Antwort mit dem richtigen ausstehenden Request zu korrelieren kann sich nicht aufIn-Reply-Toverlassen, wie es gewöhnliche Antworten tun (cbmailservices'send()exponiert nicht, welcheMessage-IDdie ausgehende Genehmigungs-E-Mail selbst erhielt), es geschieht also stattdessen über ein in der Betreffzeile eingebettetes[bxagents:<requestID>]-Tag - dieselbe Technik, die echte E-Mail-basierte Support-Ticket-Systeme aus demselben Grund nutzen. Die erste Zeile einer Antwort wird gegen die eigenen erlaubten Entscheidungen des Requests abgeglichen (exakt oder als Präfix, ohne Berücksichtigung von Groß-/Kleinschreibung); eine nicht erkannte Antwort wird unverändert durchgereicht statt erneut angefragt, überlassen an bx-ais eigenen HITL-Koordinator zur Ablehnung.
WhatsApp Business Cloud API - Webhook-getrieben, nicht verbindungsgetrieben
WhatsAppCloudGateway ist anders geformt als jedes andere Push-Style-Gateway: Meta ruft uns an, über einen öffentlichen Webhook, statt dass dieses Gateway seine eigene ausgehende Verbindung hält (ein Poll-Task oder ein Websocket). Es erweitert bx-ais BaseGateway direkt, nicht ScheduledGatewayBase - es gibt keinen Scheduler-Task oder Socket zu verwalten, nur ein generiertes handlers/WhatsAppCloud.bx (geschrieben, wann immer ein whatsapp-cloud-Gateway-Eintrag existiert), verdrahtet mit zwei festen Routen:
get( "/webhooks/whatsapp-cloud" ).toHandler( "WhatsAppCloud.verify" )
post( "/webhooks/whatsapp-cloud" ).toHandler( "WhatsAppCloud.process" )
Beide Actions sind dünne Passthroughs in die eigenen handleVerify()/handleWebhook() des Gateways - verify beantwortet Metas Abo-Handshake (GET ?hub.mode=subscribe&hub.verify_token=...&hub.challenge=..., echot die Challenge nur als reinen Text zurück, wenn Modus und Token übereinstimmen, zeitkonstant verglichen); process verifiziert Metas eigenen X-Hub-Signature-256-Header (HMAC-SHA256 über den exakten rohen POST-Body - event.getHTTPContent(), nie erneut geparstes/serialisiertes JSON, was die Bytes ändern und die Signatur brechen würde), bevor irgendetwas geparst oder dispatcht wird. Das ist ein genuin anderes Schema als bx-ais eigenes HttpGateway/GatewaySecurity (andere Header-Namen, andere HMAC-Konstruktion), es wird hier also nicht wiederverwendet - siehe den eigenen Docblock der Klasse.
Direkt portiert aus Hermes Agents eigenem echtem, produktivem WhatsApp-Cloud-Adapter (gateway/platforms/whatsapp_cloud.py, MIT-lizenziert) - der Verify-Handshake, das Signaturschema, der Webhook-Payload-Walk (entry[].changes[].value.{messages,contacts}), die ausgehenden Nachrichten-/Interactive-Button-Formen (≤3 erlaubte Entscheidungen werden als native Buttons gerendert, 4+ als tippe-zum-Öffnen-Liste, passend zu WhatsApps eigenen dokumentierten Grenzen) und die Längenbegrenzungen (4096-Zeichen-Nachrichten, 20-Zeichen-Button-Labels, 1024-Zeichen-Interactive-Body-Text) wurden alle in dieser Session direkt aus jener Quelle gelesen, nicht von Grund auf neu implementiert. Eingehende Nachrichten werden anhand ihrer eigenen wamid dedupliziert (Meta wiederholt die Webhook-Zustellung bei jeder Nicht-200-Antwort bis zu 7 Tage lang) über einen begrenzten FIFO-Cache, gespiegelt an Hermes' eigenem _dedup_wamid.
v1-Umfang, passend zu Hermes' eigener dokumentierter Einschränkung: Cloud-API-DMs haben keine separate "Chat"-Entität - chat_id IST die wa_id des Absenders - und Gruppennachrichten (die ein eigenes chat-Feld tragen, das die Gruppen-JID identifiziert) sind außerhalb des Umfangs; Medien (Bild/Video/Dokument/Audio) werden nicht heruntergeladen, nur eine Beschriftung, falls vorhanden. Jedes andere Push-Style-Gateway teilt dieselbe oben dokumentierte Eine-Instanz-pro-Typ-Registry-Obergrenze - whatsapp-cloud bildet keine Ausnahme.
Die eigenen ColdBox-Request-Context-Aufrufe des generierten handlers/WhatsAppCloud.bx (event.getHTTPContent()/event.getHTTPHeader()/event.renderData(), rcs über das URL-Scope gemergte Query-Parameter für den GET-Handshake) sind die dokumentierten, standardmäßigen ColdBox-REST-Handler-Idiome - aber anders als die eigene Signatur-/Dispatch-Logik des Gateways (in dieser Session gründlich unit-getestet und empirisch gegen echtes HMAC-/JSON-Verhalten verifiziert), wurde diese spezifische generierte Routen-Verdrahtung NICHT gegen einen echten ColdBox-Boot geprüft. Siehe known-limitations.md.
Microsoft Teams - Bot-Framework-Activity-Protokoll
TeamsGateway ist Webhook-getrieben, auf dieselbe Weise wie WhatsAppCloudGateway - es erweitert BaseGateway direkt, und Microsofts eigener Bot-Connector-Dienst ruft uns an, über eine einzelne generierte Route:
post( "/webhooks/teams" ).toHandler( "Teams.process" )
Anders als bei WhatsApp Cloud gibt es keinen GET-Verify-Handshake (das Bot Framework hat kein Äquivalent zu Metas hub.challenge) - jede eingehende Activity kommt als signiertes POST an, verifiziert über ein Bearer-JWT im Authorization-Header statt über eine HMAC-Signatur über den Body. Das JWT wird gegen die eigene JWKS des Bot Connectors geprüft (https://login.botframework.com/v1/.well-known/openidconfiguration → dessen jwks_uri) - RS256-Signatur, aud muss der eigenen konfigurierten appId des Bots entsprechen, iss muss dem festen Aussteller-String des Bot Connectors entsprechen (https://api.botframework.com), beides mit einer 5-Minuten-Uhrabweichungstoleranz. Das ist echte RSA-/JWT-Verifikation, aufgebaut aus BoxLangs eigener Java-Interop (java.security.Signature, java.security.KeyFactory, java.math.BigInteger) - keine externe JWT-Bibliothek. Ausgehende Aufrufe nutzen ein separates OAuth2-Client-Credentials-Token (abgerufen von login.microsoftonline.com/{tenantId}/oauth2/v2.0/token, gecacht und 60s vor dem angegebenen Ablauf neu abgerufen).
Portiert aus Vercel Eves echtem Teams-Kanal (packages/eve/src/public/channels/teams/, MIT-lizenziert) - der OAuth2-Ablauf, das JWT-Verifikationsschema, das REST-Tripel v3/conversations/{id}/activities[/{activityId}] und die Adaptive-Card-Human-in-the-Loop-Form (Schema 1.5, ein Action.Submit-Button pro erlaubter Entscheidung) spiegeln alle diese Implementierung. Hermes Agents eigenes msgraph_webhook.py ist trotz des ähnlichen "Microsoft-Webhook"-Namens unabhängig davon - es implementiert Microsoft-Graph-Change-Notification-Webhooks (Postfach-/Laufwerk-/Listen-Ressourcenänderungs-Events, eine andere Microsoft-Produktoberfläche ganz ohne funktionierendes ausgehendes Teams-Messaging) und nichts daraus wurde hierher portiert.
v1-Umfang ist nur persönliche (1:1-DM-)Konversationen - Gruppenchats und kanalweite Nachrichten brauchen Bot-Mention-Gating und ein anderes Reply-Threading-Modell, das Eve selbst implementiert, dieser Port aber nicht, passend zum eigenen DM-first-v1-Umfang jedes anderen Push-Style-Gateways. Es wird eine Nachrichten-Chunk-Grenze von 4000 Zeichen genutzt (Eves eigene Adaptive-Card-Text-Truncation-Konstante) statt der echten 80-KiB-Grenze des Bot-Framework-Protokolls, aus Gründen der UI-Lesbarkeit.
Die Bot-Connector-JWKS wird einmal abgerufen und für die Lebensdauer der Gateway-Instanz gecacht - falls Microsoft je seine Signierschlüssel rotiert, ohne dass eine passende kid bereits gecacht ist, würde die Verifikation zu scheitern beginnen, bis das Gateway (und damit die ganze App) neu startet. Für v1 ist keine periodische Cache-Invalidierung gebaut. Die JWT-Verifikationslogik selbst wurde in dieser Session empirisch gegen ein echtes, lokal generiertes RSA-Schlüsselpaar und handsignierte Test-JWTs verifiziert (gültige Signatur akzeptiert, manipulierte Signatur/falsche Audience/abgelaufenes Token alle mit 401 abgelehnt) - nicht nur gegen Eves Quellcode gelesen.
Twilio SMS - ein genuin anderes Signaturschema, und ein zweigleisiges Response-Modell
TwilioGateway ist Webhook-getrieben, auf dieselbe Weise wie WhatsAppCloudGateway/TeamsGateway:
post( "/webhooks/twilio" ).toHandler( "Twilio.process" )
Zwei Dinge machen Twilios eigenen Webhook-Vertrag sinnvoll anders als jedes andere Gateway in diesem Projekt, beide treu portiert aus Vercel Eves echtem Twilio-Kanal (packages/eve/src/public/channels/twilio/, MIT-lizenziert):
- Der eingehende Body ist form-urlencoded (
Body,From,To,MessageSid,AccountSid), nicht JSON -TwilioGatewayparst ihn selbst (java.net.URLDecoder), keine JSON-Deserialisierung beteiligt. - Die Signaturprüfung ist
X-Twilio-Signature: HMAC-SHA1, base64-kodiert (jedes andere Webhook-Gateway in diesem Projekt nutzt HMAC-SHA256, hex-kodiert) - die Signing-Basis ist die exakte Request-URL, gefolgt von jedem POST-Parameter, dessen eigeneskey & valuedirekt verkettet (keine Trennzeichen), alphabetisch nach Schlüssel sortiert. Da die URL selbst Teil dessen ist, was signiert wird, braucht ein hinter einem Reverse-Proxy oder Tunnel laufendes Projekt (wo die von ColdBox überevent.getUrl()gesehene URL nicht dem entspricht, wohin Twilio tatsächlich gepostet hat) den optionalenpublicUrl-Konfigurations-Override - dieselbe Art von Falle, die Eves eigene Dokumentation für dessenwebhookUrl-Option markiert. - Die synchrone Webhook-Antwort ist immer ein leeres TwiML
<Response></Response>- Twilios eigenes klassisches zweigleisiges Modell. Die echte Agentenantwort wird später, außerhalb des Kanals, über einen separatendeliver()-REST-Aufruf an die Messages-API gesendet, sobald der asynchrone Turn von GatewaySession abgeschlossen ist - passend zu Eves eigenememptyTwilioResponse()exakt (Eve nutzt nie eine synchrone TwiML-<Message>, um inline zu antworten).
Ausgehende Sends sind Basic-Auth-REST-Aufrufe an POST /2010-04-01/Accounts/{AccountSid}/Messages.json, form-kodierter Body (To, Body, und entweder From oder MessagingServiceSid, falls konfiguriert). v1 ist nur SMS-Text - Eves eigener Twilio-Kanal ist ein kombinierter SMS+Sprach-Kanal (/voice-Routen, <Gather>/<Say>-TwiML, Anruftranskription); nichts der sprachspezifischen Teile wurde portiert.
SMS hat überhaupt keine native Button-/Karten-Affordanz (über Eves eigene Dokumentation bestätigt), Human-in-the-Loop ist also auf dieselbe Weise verschlechtert wie bei E-Mail - getDeclaredCapabilities() lässt "interactiveActions" weg (und "threads", da Twilios klassische Messages-API auch kein natives Antwort-/Zitat-Konzept hat). requestHumanInteraction() sendet eine reine Text-SMS, die die erlaubten Entscheidungen auflistet; anders als E-Mail (das ein [bxagents:<requestID>]-Tag in der Betreffzeile einbettet, um die eventuelle Antwort zu korrelieren) hat SMS keine Betreffzeile zum Taggen - der ausstehende Request wird stattdessen nach der eigenen Telefonnummer des Absenders (conversationID) geschlüsselt, eine v1-Vereinfachung, die höchstens einen offenen HITL-Request pro Telefonnummer gleichzeitig annimmt.
Anders als Eve (das überhaupt keine Längenbegrenzungslogik hat - durch Grep über dessen Quellcode als fehlend bestätigt - und sich vollständig auf Twilios eigene serverseitige Segmentierung verlässt), wendet TwilioGateway trotzdem MessageChunker bei 1600 Zeichen an (Twilios eigene dokumentierte Einzelnachrichten-Konkatenationsgrenze), für Konsistenz mit dem Chunking-Verhalten jedes anderen Gateways. Das HMAC-SHA1-Signaturschema wurde in dieser Session gegen einen unabhängig berechneten Python-hmac-/hashlib-Referenzwert querverifiziert, bevor der BoxLang-Implementierung vertraut wurde, dieselbe Disziplin wie bei WhatsApp Clouds eigenem HMAC-SHA256-Schema.
GitHub - über @mention geschützte Issue-/PR-Kommentar-Threads
GitHubGateway behandelt jedes Issue, jede PR oder jeden Inline-Review-Kommentar-Thread als Chat-Konversation - der Agent antwortet, wenn er in einem Kommentar explizit per @mention erwähnt wird, und antwortet, indem er einen neuen Kommentar in denselben Thread postet. Webhook-getrieben, auf dieselbe Weise wie jedes andere Gateway in diesem Abschnitt:
post( "/webhooks/github" ).toHandler( "GitHub.process" )
Portiert aus Vercel Eves echtem GitHub-Kanal (packages/eve/src/public/channels/github/, MIT-lizenziert) - die X-Hub-Signature-256-Verifikation ist bestätigt die identische Konstruktion wie Metas eigenes WhatsApp-Cloud-Schema (HMAC-SHA256 über den rohen Body, hex, sha256=-Präfix) - das einzige Webhook-Gateway in diesem Projekt, das den exakten Signaturalgorithmus eines anderen wiederverwendet, statt einen eigenen zu brauchen. Nur issue_comment- und pull_request_review_comment-Events mit action: "created" werden dispatcht (passend zu Eves eigenen ausschließlich standardmäßig behandelten Event-Arten - issues/pull_request/check_suite/check_run/workflow_run haben auch bei Eve keinen Standard-Dispatch und sind hier nicht verdrahtet); jede andere Event-Art wird bestätigt (200), aber ignoriert, um GitHubs Retry-/Hook-bei-Fehler-deaktivieren-Verhalten für Events zu vermeiden, auf die dieses Gateway nicht reagiert.
Das Dispatch-Gate ist eine echte @mention-Anforderung, portiert aus Eves eigenem extractGitHubCommentTrigger(): Ein Kommentar erreicht den Agenten nur, wenn er @<botName> gefolgt von Stringende oder einem Nicht-Identifier-Zeichen enthält (ein Bot namens mybot feuert also nie bei einem Kommentar, der @mybot2 erwähnt) - in dieser Session über einen echten Regex-Lookahead-Smoke-Test bestätigt, bevor dem vertraut wurde. Das gematchte @mention-Token wird aus dem Text entfernt, bevor er den Agenten erreicht. Bot-Loop-Verhinderung spiegelt Eves eigenen dreiteiligen Schutz: Jeder Kommentar, dessen Autor GitHubs eigenen type: "Bot" hat, dessen Login zu {botName}[bot] passt, oder dessen Body den eigenen Marker <!-- bxagents:posted --> dieses Gateways enthält (an jeden von ihm geposteten Kommentar angehängt), wird von vornherein ignoriert, selbst wenn er zufällig eine Mention enthält.
Eine "Konversation" wird durch eine von zwei Formen identifiziert, passend zu Eves eigenem Modell: repo:{owner}/{repo}:issue:{issueNumber} für einen gewöhnlichen Issue-/PR-Kommentar-Thread, oder repo:{owner}/{repo}:review-comment:{reviewThreadRootCommentId} für einen Inline-PR-Review-Kommentar-Thread - Antworten auf einen Review-Thread gehen immer an den Thread-Root-Kommentar (comment.in_reply_to_id ?? comment.id), nicht an den konkreten Kommentar, auf den geantwortet wird, sodass ein Mehrfach-Nachrichten-Hin-und-her ein Thread bleibt. Ausgehende Antworten posten an repos/{owner}/{repo}/issues/{issueNumber}/comments (gewöhnliche Threads) oder repos/{owner}/{repo}/pulls/{pullRequestNumber}/comments/{reviewCommentId}/replies (Review-Threads).
v1-Auth ist ein einfaches Personal-Access-Token (tokenEnvVar), nicht Eves eigener GitHub-App-JWT-+-Installations-Token-Ablauf - einfacher und direkter portierbar für einen ersten Wurf (Eve selbst unterstützt einen Bypass mit vorab aufgelöstem Token, genau dafür - genau das ist es, worauf das hier abbildet). Ein zukünftiger GitHub-App-Modus ist eine natürliche Erweiterung, hier nicht gebaut. Anders als Eve (das gar kein Delivery-ID-Dedup hat, durch Lesen seines Quellcodes als fehlend bestätigt), dedupliziert GitHubGateway per X-GitHub-Delivery über einen begrenzten FIFO-Cache, passend zu WhatsApp Clouds eigener wamid-Dedup-Disziplin.
Kein Repo-Checkout/Code-Editing (Eves eigenes checkout.ts, das das Repo in eine Sandbox klont, damit der Agent Code lesen/bearbeiten kann) wurde portiert - dies ist nur eine Kommentar-rein-Kommentar-raus-Chat-Oberfläche. Human-in-the-Loop ist auf dieselbe Weise verschlechtert wie bei Twilio (keine native Button-/Karten-Affordanz) - requestHumanInteraction() postet einen Kommentar, der den Menschen bittet, den Bot in einer Antwort mit einer der erlaubten Entscheidungen erneut per @mention zu erwähnen, korreliert nach conversationID (kein Tag pro Request), dieselbe v1-Vereinfachung, die auch Twilios eigener HITL-Fallback nutzt.
Es gibt keinen Typ "whatsapp-personal". Die inoffizielle persönliche Konto-Bridge (WhatsApps Multi-Device-Web-Protokoll, die Art, wie Hermes Agent sie über einen Node.js-/Baileys-Subprozess erreicht) wurde recherchiert, aber bewusst nicht gebaut - die eine MIT-lizenzierte native Java-Option (Cobalt, com.github.auties00:cobalt) zog sich in der tatsächlich auf Maven Central veröffentlichten Version eine kommerzielle/proprietäre Abhängigkeit (com.aspose:aspose-words) hinein, und ein Subprozess-Bridge-Port wurde zugunsten eines nativen JVM-Ansatzes zurückgestellt. Wird type: "whatsapp-personal" in einem gateways/*-Eintrag deklariert, schlägt die Validierung mit einem "unbekannter Typ"-Fehler fehl, wie bei jedem anderen nicht unterstützten Typ. Siehe docs/known-limitations.md für die vollständige Untersuchung.
Signal - eine vierte Transportform, gegen einen externen signal-cli-Daemon
SignalGateway ist nicht Webhook-getrieben wie WhatsApp Cloud/Teams/Twilio/GitHub oben, und es ist auch kein Websocket wie Slack/Discord - es erweitert ScheduledGatewayBase auf dieselbe Weise wie Telegram/Slack/Discord/E-Mail, aber seine eigene Verbindung sind Server-Sent Events: ein einzelner, langlebiger GET {httpUrl}/api/v1/events?account=...-Request, offen gehalten über die asynchrone API von java.net.http.HttpClient (sendAsync() + BodyHandlers.ofLines()), wobei ein JSON-Event pro Zeile gelesen wird, während signal-clis eigener Daemon sie über denselben Response-Body pusht. Ausgehende Sends sind einfaches JSON-RPC 2.0 (POST {httpUrl}/api/v1/rpc, {"jsonrpc":"2.0","method":"send","params":{...},"id":...}) gegen denselben Daemon.
Es gibt keine offizielle Signal-Bot-API - SignalGateway spricht ausschließlich mit signal-cli, das im eigenen daemon --http-Modus läuft, eine externe Voraussetzung, von der dieses Gateway abhängt, die es aber nicht selbst verwaltet, dieselbe Beziehung, die EmailGateway zu einem externen IMAP-/SMTP-Server hat. Portiert aus Hermes Agents eigenem echtem Signal-Kanal - die SSE-/JSON-RPC-Drahtformen, die Reconnect-Backoff-Konstanten (2s bis 60s exponentiell, +20% Jitter) und der 30s/120s-Idle-Watchdog werden alle direkt aus jener Quelle gelesen, nicht von Grund auf neu implementiert.
Einen funktionierenden signal-cli-Daemon zu bekommen ist ein echter, manueller, einmaliger Einrichtungsschritt vollständig außerhalb dieses Projekts: signal-cli installieren, mit einem echten Signal-Konto registrieren/verknüpfen (signal-cli link oder register, beide brauchen eine echte Telefonnummer und einen Geräteverknüpfungs-QR-/Verifikationsschritt), dann signal-cli -a <account> daemon --http=127.0.0.1:8080 ausführen und diesen Prozess am Laufen halten (ein systemd-Dienst oder Container-Sidecar, nicht etwas, das bxAgents serve selbst startet). SignalGateways eigenes onConnect() scheitert laut mit MissingConfig, falls account nicht gesetzt ist, kann aber den Daemon selbst weder erkennen noch starten - ein zur Verbindungszeit unerreichbares httpUrl äußert sich als gewöhnlicher Reconnect-Backoff-Zyklus, nicht als schneller Fehlschlag.
v1 ist nur DM - Hermes' eigener Signal-Kanal behandelt Gruppenkonversationen standardmäßig als opt-in/aus, und das ist der einzige hier portierte Modus. Human-in-the-Loop ist auf dieselbe Weise verschlechtert wie Twilio/GitHubs Fallback (getDeclaredCapabilities() lässt "interactiveActions" weg) - Signal-Lesebestätigungen/Reaktionen sind in signal-clis eigener API nur schreibbarer kosmetischer Status, kein echter Antwortkanal, requestHumanInteraction() fällt also auf eine reine Textnachricht zurück, die die erlaubten Entscheidungen auflistet, korreliert nach conversationID wie Twilios eigener telefonnummer-geschlüsselter Fallback. Die JSON-RPC-/SSE-Parsing-Logik (handleSseEvent(), Zitat-Threading, Gruppen-Nachrichten-Filterung, HITL-Entscheidungsabgleich) wurde durch echte öffentliche Methoden gesteuert, wobei nur die äußersten rpcCaller-/connector-I/O-Aufrufe gestubbt sind, dieselbe Naht-Test-Disziplin wie bei jedem anderen Gateway - aber in dieser Umgebung war kein echter signal-cli-Daemon verfügbar, sodass der tatsächliche asynchrone Verbindungslebenszyklus (Öffnen des SSE-Streams, die Reconnect-mit-Backoff-Schleife gegen eine tatsächlich flackernde Verbindung, der JSON-RPC-Roundtrip gegen einen Live-Daemon) nie Ende-zu-Ende geprüft wurde. Die Interop-Kette von java.net.http.HttpClient selbst wurde als solide bestätigt - ein eigenständiger Smoke-Test erreichte einen echten java.net.ConnectException an der echten Netzwerkgrenze gegen eine unerreichbare Testadresse, was beweist, dass die Verkabelung funktioniert, obwohl sie nie einen Live-Daemon berührt hat.
GatewaySession - wiring the agent to every push-style gateway
Jedes Projekt mit mindestens einem Push-Style-Gateway-Eintrag erhält außerdem ein generiertes interceptors/GatewaySessionBootstrap.bx, das eine einzige bx-ai-GatewaySession baut, die jedes Push-Style-Gateway im Projekt bündelt, an den Root-Agenten des Projekts gebunden ist, und startet, sobald ColdBox selbst mit dem Laden fertig ist:
// interceptors/GatewaySessionBootstrap.bx (GENERATED)
class {
function afterConfigurationLoad( event, interceptData ) {
var wirebox = getController().getWireBox()
var agent = wirebox.getInstance( "GeneratedAgent" )
var gatewaySession = aiGatewaySession(
agent : agent,
gateways : [ aiGatewayRegistry().get( "telegram" ) ],
policy : "queue",
maxQueueDepth: 50
)
gatewaySession.start()
application.bxaiGatewaySession = gatewaySession
}
}
Die generierte Variable heißt bewusst gatewaySession, nicht session - session ist ein reservierter BoxLang-/ColdBox-Scope-Name (wie request/server/url/form/cgi/thread), und eine lokale Variable, die einen dieser Namen wiederverwendet, kann mit dem lebenden Scope kollidieren, statt sich wie eine gewöhnliche lokale Variable zu verhalten.
Der aiGatewayRegistry().get(...)-Schlüssel ist immer der Gateway-TYPE-String ("telegram", "slack", "discord", "email", ...) - gegen bx-ais echten GatewayRegistry.register()-Quellcode bestätigt, der immer nach dem eigenen festen getName() der Gateway-Klasse schlüsselt, nie nach etwas vom Aufrufer Übergebenem. Eine echte Konsequenz: Zwei gateways/*-Einträge desselben Push-Style-Typs kollidieren auf demselben Registry-Slot, projektweit - die zweite Registrierung überschreibt still die erste. Es gibt heute keinen Pro-Eintrag-Alias - für jede zusätzliche Plattform-Account einen eigenen Typ verwenden, oder auf Multi-Instanz-Unterstützung warten.
Die Richtlinie der GatewaySession wird über einen optionalen gatewaySession-Block am Agent.bx des Root-Projekts gesteuert:
// Agent.bx
class extends="bxModules.bxai.models.runnables.AiAgent" {
function init() {
super.init( name: "...", model: aiModel( provider: "..." ) )
return this
}
function configure() {
return {
gatewaySession: { policy: "queue", maxQueueDepth: 50 } // both optional - these are the defaults
};
}
}
policy muss reject/queue/steer/interrupt sein (bx-ais eigenes GatewaySession-Richtlinienvokabular - siehe GatewaySession unten) - zur build-Zeit geprüft, sodass ein Tippfehler laut fehlschlägt, statt als Laufzeitfehler zu erscheinen, sobald die App zum ersten Mal bootet.
v1-Einschränkung: genau eine GatewaySession, immer an den Root-Agenten des Projekts gebunden - passend zum bestehenden Präzedenzfall, dass die HTTP-Exposure exposes: "agent" ebenfalls immer nur den Root-Agenten betrifft. Ein Projekt mit Subagenten kann noch nicht unterschiedliche Gateways zu unterschiedlichen Subagenten routen.
Was jede Richtlinie tatsächlich mit einer Nachricht tut, die eintrifft, während noch ein Turn läuft:
flowchart TD
M["a message arrives on thread T"] --> B{"is a run already<br/>in flight on T?"}
B -->|"no"| D["dispatch a new turn.<br/>The reply streams back through<br/>the gateway the message came from."]
B -->|"yes"| P{"policy"}
P -->|"reject"| R["Immediate 'busy' reply.<br/>Nothing is queued - the sender must resend."]
P -->|"queue<br/>(the default)"| Q["Enqueue, up to maxQueueDepth.<br/>Runs as its own turn once<br/>the current one finishes."]
P -->|"steer"| ST["agent.steerRun( T, text )<br/>Spliced into the SAME run at its next<br/>checkpoint - never a second turn."]
P -->|"interrupt"| I["agent.cancelRun( T ), AND enqueue.<br/>The current turn winds down at its next<br/>checkpoint, then this message runs."]
Q --> OVER{"queue already at<br/>maxQueueDepth?"}
I --> OVER
OVER -->|"yes"| R
style D fill:#d4edda,stroke:#155724
style R fill:#f8d7da,stroke:#721c24
"Steer" bedeutet hier Hermes Agents zerstörungsfreies Splicing - der laufende Turn läuft weiter, und der neue Text wird in ihn eingefügt. Es bedeutet nicht, was Eves turnPolicy: "steer" bedeutet (den aktiven Turn abbrechen und einen Ersatz starten); dieses Verhalten heißt in diesem Vokabular interrupt.
Weder cancelRun() noch steerRun() ist sofort. Beide werden signalisiert und wirken erst am nächsten Checkpoint des Laufs (vor dem nächsten LLM- oder Tool-Aufruf), interrupt bedeutet also "den aktuellen Turn bitten, bald abzuklingen", nicht "synchron ersetzen".
Wie ein Push-Style-Gateway verbunden bleibt: der geteilte ColdBox-Scheduler
Statt einer neuen Hintergrundschleifen-Primitive erreichen Push-Style-Gateways das eigene, lebende ColdBox-Scheduler-Singleton der App (appScheduler@coldbox - dasselbe, unter dem auch ein handgeschriebenes schedules/Scheduler.bx, falls das Projekt eines hat, läuft) und registrieren ihre eigenen benannten Task(s) dynamisch darin - z. B. ein wiederkehrender Long-Poll-Task für Telegram. Ein geteilter Scheduler, jedes Push-Style-Gateway registriert seine eigenen Tasks darin - nie ein Scheduler pro Gateway, und nie im Konflikt mit den eigenen Cron-Jobs eines Projekts.
Logging
Jedes Push-Style-Gateway schreibt in seine eigene gateway-<type>-Logdatei (z. B. gateway-telegram) über BoxLangs writeLog(), statt in ein geteiltes/Standard-App-Log - sodass eine Bedienerin genau die Plattform verfolgen kann, die sie interessiert, ohne Rauschen von allem anderen, was die App sonst loggt.