Adapter Capability Contract
A declared supports_* capability is verified at build time, so an adapter can't advertise something it can't do
A declared supports_* capability is verified once, at adapter build time — an unbacked flag fails loudly before any turn instead of raising mid-conversation.
An agent runs behind a Bot; when you build that bot with a custom adapter, the contract catches a broken declaration before the first message.
from praisonaiagents import Agent, BasePlatformAdapter, SendResult
from praisonaiagents.bots import PlatformCapabilities
agent = Agent(name="assistant", instructions="Be helpful")
class MyBot(BasePlatformAdapter):
capabilities = PlatformCapabilities(supports_edit=True) # unbacked → caught at build
async def connect(self, *, is_reconnect: bool = False) -> bool:
return True
async def disconnect(self) -> None:
...
async def send(self, chat_id, content, *, reply_to=None, metadata=None) -> SendResult:
return SendResult(ok=True, chat_id=chat_id)
async def get_chat_info(self, chat_id):
return {"id": chat_id}
# from praisonai_bot.bots import Bot
# Bot("mybot", agent=agent)
# -> CapabilityContractError: MyBot declares supports_edit=True but does not
# override edit_message with a callable implementationQuick Start
- Declare what you support truthfully
Set
supports_edit=Trueand overrideedit_message— the adapter passes verification.python from praisonaiagents import BasePlatformAdapter, SendResult from praisonaiagents.bots import PlatformCapabilities, verify_capability_contract class MyBot(BasePlatformAdapter): capabilities = PlatformCapabilities(supports_edit=True) async def connect(self, *, is_reconnect: bool = False) -> bool: return True async def disconnect(self) -> None: ... async def send(self, chat_id, content, *, reply_to=None, metadata=None) -> SendResult: return SendResult(ok=True, chat_id=chat_id) async def get_chat_info(self, chat_id): return {"id": chat_id} async def edit_message(self, chat_id, message_id, content) -> SendResult: await acme_api.edit(chat_id, message_id, content) return SendResult(ok=True, message_id=message_id, chat_id=chat_id) print(verify_capability_contract(MyBot())) # [] — contract holds - The verifier catches a broken promise
Remove the override and the same declaration fails at build time — not mid-turn.
python from praisonaiagents import BasePlatformAdapter, SendResult from praisonaiagents.bots import PlatformCapabilities, enforce_capability_contract class MyBot(BasePlatformAdapter): capabilities = PlatformCapabilities(supports_edit=True) # no edit_message override async def connect(self, *, is_reconnect: bool = False) -> bool: return True async def disconnect(self) -> None: ... async def send(self, chat_id, content, *, reply_to=None, metadata=None) -> SendResult: return SendResult(ok=True, chat_id=chat_id) async def get_chat_info(self, chat_id): return {"id": chat_id} enforce_capability_contract(MyBot()) # -> CapabilityContractError: MyBot declares supports_edit=True but does not # override edit_message with a callable implementation
How It Works
The check iterates CAPABILITY_BACKING and asks, per declared flag, whether the backing method is overridden.
| Capability flag | Backing method | Behaviour when declared but unbacked |
|---|---|---|
supports_edit | edit_message | CapabilityContractError at build; NotImplementedError if the check is skipped and the method is called |
supports_delete | delete_message | Same |
The two check entry points
Two functions read the same contract; one reports, one raises.
verify_capability_contract(adapter) -> list[str] returns a list of human-readable violation strings — empty when the contract holds — and never raises. Use it to log a warning without failing startup.
from praisonaiagents.bots import verify_capability_contract
for violation in verify_capability_contract(adapter):
logger.warning(violation)enforce_capability_contract(adapter) -> None raises a single CapabilityContractError naming every violation (joined by "; "), or does nothing. It is called automatically by Bot._build_adapter and by the gateway’s _build_channel_adapter.
from praisonaiagents.bots import enforce_capability_contract
enforce_capability_contract(adapter) # raises on any violationWhere the check runs automatically
Both build paths enforce the contract for you — there is no flag or environment variable to opt in.
When you build a Bot(...) with a wrapper adapter (praisonai-bot), the contract is verified right after adapter construction, guarded by a lazy import so an older core release simply skips the check. On the gateway construction path (gateway/server.py::_build_channel_adapter), a violation is caught the same way and the channel is recorded as degraded — visible in gateway status — rather than surfacing later inside a live turn.
Backward compatibility
Built-in adapters already honour the contract, so nothing needs to change.
All built-in adapters that declare supports_edit=True (Slack, Telegram, Discord) already override edit_message. No built-in adapter declares supports_delete=True, so no built-in adapter trips the contract. Capabilities left False continue to degrade cleanly, unchanged.
Adding a new capability flag
Extend the contract by pairing a new flag with a base method that has a default.
- Add the supports_* field
Add the field to
PlatformCapabilities(dataclass plusto_dict/from_dict). - Pick a backing method
Choose a method the base class provides a default for.
- Add the pair to CAPABILITY_BACKING
Register
"supports_new": "new_method"in the fail-closed map. - Gate the base default (optional)
If the base default should raise on a declared-but-unbacked call (parity with
edit_message), gate the default on the flag and raiseNotImplementedErrorotherwise.
Best Practices
Declare only what your platform actually does
A lie in PlatformCapabilities is now caught immediately at build time. Declare supports_edit or supports_delete only when a real implementation backs them.
Prefer enforce_ in production, verify_ in dev tooling
enforce_capability_contract fails startup loudly. verify_capability_contract returns violations without raising, so dev tooling can log a warning and keep going.
New flag → new backing method entry
Every entry in CAPABILITY_BACKING is a fail-closed guard. Adding a flag with its backing method keeps future regressions from advertising an unimplemented capability.
Related
The PlatformCapabilities descriptor whose flags this contract verifies.
Build a channel — declare capabilities and back them honestly.
Bot Platform PluginsRuntime registration and discovery of platform adapters.
Custom Gateway Channel (Zero-Code)The gateway construction path that records a violation as a degraded channel.