This document explains the plugin interface used by MAAT-RPG and the shared MAAT runtime.
Plugins are loaded from configured plugin roots.
The loader supports:
- a folder containing
plugin_main.py - a direct
plugin_*.pyfile
Folders beginning with _ are ignored.
A minimal plugin looks like this:
class Plugin:
type = "chat"In practice, most plugins live in a folder like:
my_plugin/
plugin_main.py
Plugins can declare:
type = "chat"type = "stream"
Used for gameplay systems, commands, state changes, and response processing.
Used for token-level behavior during generation, for example TTS.
Called after plugins are loaded.
Use it for:
- initialization
- startup checks
- optional setup actions
- menu or intro boot logic
Example:
def on_startup(self, context=None):
passCalled before the model is queried.
Typical use cases:
- story triggers
- battle triggers
- input rewriting
- quest progression
- memory capture
Valid return styles:
(False, None)-> not handled, continue normally(False, new_input)-> continue with modified input(True, output)-> handled; stop normal flow and useoutputstr-> replace input text
Called after a full reply has been generated.
Typical use cases:
- append HUD or logs
- update state
- soften formatting
- store memories
Valid return styles:
None-> leave reply unchangedstr-> replace reply
Optional late-stage guard hook.
This is intended for final stabilization after normal post-processing.
Typical use cases:
- hallucination risk mitigation
- cautionary rewording
- blocking hard claims when confidence is too low
Valid return styles:
None-> keep current replystr-> replace final reply
Optional companion hook for before_final_response(...).
Use this when a final-response guard should only activate under certain conditions, such as a user setting.
Return:
True-> guard is activeFalse-> guard is inactive
These hooks are relevant for type = "stream" plugins.
Called before streaming output begins.
Called for each streamed token or chunk.
Called after streaming finishes.
Typical use case:
- text-to-speech
There are three main ways to expose plugin commands.
General command handler.
Example:
def command(self, cmd, context=None):
if cmd == "/mycmd":
return "Hello"
return NonePossible return styles:
None-> not handledstr-> immediate output(True, reply)-> explicitly handled
Dictionary-based registration.
Example:
commands = {
"/mycmd": {"de": "Mein Befehl", "en": "My command"}
}This lets the plugin manager register commands directly on the router with descriptions.
List-based registration.
Example:
available_commands = ["/mycmd", "/mycmd info"]If a plugin exposes command(...) but no explicit command list, the loader can also register a fallback command name.
Many hooks receive a context dictionary.
Depending on the runtime stage, it may contain things like:
conversationlast_user_inputpm- battle or story state references
- MAAT meta information
- profile-related state
Plugins should treat context keys defensively:
- check whether a key exists
- do not assume every context is complete
A plugin should usually own one responsibility:
- quests
- battle
- memory
- TTS
- uncertainty guard
This keeps debugging and maintenance manageable.
Older plugins may still exist in the ecosystem. The loader already tries to tolerate some older call signatures, so new plugins should prefer the modern (value, context=None) style without breaking old installs unnecessarily.
If your plugin stores runtime state, use the MAAT path helpers so data lands in the active external profile path, not in the repository folder or app bundle.
If your plugin prints user-facing text, support:
- German
- English
Hooks should fail softly whenever possible. A plugin should not bring down the entire game because a non-critical side action failed.
class Plugin:
type = "chat"
commands = {
"/echo": {"de": "Echo ausgeben", "en": "Print echo"}
}
def on_startup(self, context=None):
pass
def command(self, cmd, context=None):
if cmd.startswith("/echo "):
return cmd[6:]
return None
def before_chat(self, user_input, context=None):
return False, None
def after_response(self, reply, context=None):
return replyclass Plugin:
type = "stream"
def before_stream(self, text):
pass
def on_token(self, token):
pass
def after_stream(self, full_text):
passIf you want to inspect the runtime behavior directly, the most relevant source files are:
shared/plugins/plugin_loader.pyshared/core/command_router.pyshared/core/streaming.pyapps/maat_rpg/basic.py
Together, these files define how plugins are discovered, called, and integrated into the MAAT-RPG turn loop.